En la lección anterior descubriste que libuv mantiene un ciclo que pregunta sin descanso "¿hay algo terminado?, ¿hay algún callback que ejecutar?". Ese ciclo es el bucle de eventos, y es la pieza que convierte a Node.js en lo que es. Todo lo demás —los callbacks, las promesas, los eventos, el servidor HTTP, la base de datos— está construido encima.

Esta es, sin exageración, la lección más importante del curso. No porque vayas a escribir código que manipule el bucle directamente (no lo harás casi nunca), sino porque el orden en que se ejecutan las cosas en Node solo tiene sentido cuando conoces sus fases. Sin ese conocimiento, tarde o temprano te encontrarás mirando una salida por consola que aparece "en el orden equivocado", un temporizador que se dispara tarde, o un servidor que responde bien en tu portátil y mal en producción, y no tendrás modelo mental para explicarlo.

Al terminar podrás predecir línea a línea la salida de cualquier programa asíncrono en Node, sabrás cuándo setImmediate gana a setTimeout y cuándo no, entenderás por qué process.nextTick se cuela delante de todo el mundo y por qué eso puede ser peligroso, y sabrás medir la métrica de salud número uno de un servidor Node: el retraso del bucle de eventos.

Contenido

  1. Qué es exactamente el bucle de eventos
  2. Las seis fases, en orden
  3. Fase 1: timers
  4. Fase 2: pending callbacks
  5. Fase 3: idle y prepare
  6. Fase 4: poll, el corazón del corazón
  7. Fase 5: check y setImmediate
  8. Fase 6: close callbacks
  9. setTimeout frente a setImmediate
  10. Las dos colas que se cuelan: process.nextTick y las microtareas
  11. Una traza completa que debes poder predecir
  12. La inanición del bucle: el peligro de nextTick
  13. setTimeout(fn, 0) no es inmediato
  14. Medir el retraso del bucle de eventos
  15. Escena Viva: por qué un cálculo síncrono degrada a todos

  1. Qué es exactamente el bucle de eventos

El bucle de eventos es un bucle while escrito en C dentro de libuv. Nada más místico que eso. Su cuerpo, muy simplificado, se parece a esto:

/* Pseudocódigo de uv_run(), en libuv */
while (hay_tareas_pendientes(loop)) {
  ejecutar_fase_timers(loop);
  ejecutar_fase_pending_callbacks(loop);
  ejecutar_fase_idle_prepare(loop);
  ejecutar_fase_poll(loop);          /* aquí es donde se espera */
  ejecutar_fase_check(loop);
  ejecutar_fase_close_callbacks(loop);
}
/* Al salir del while, el proceso termina */

Cada vuelta completa de ese while se llama tick o iteración del bucle. Y la condición hay_tareas_pendientes es exactamente la cuenta de referencias que viste en la lección anterior: mientras haya un temporizador programado, un socket abierto o una lectura de fichero en curso, el bucle sigue girando; cuando la cuenta llega a cero, el while termina y el proceso muere.

Tres ideas que hay que fijar antes de continuar:

  1. El bucle de eventos vive en el hilo principal, el mismo que ejecuta tu JavaScript. No son dos cosas en paralelo. Mientras tu callback se ejecuta, el bucle está parado esperando a que le devuelvas el control. Esta es la razón mecánica de por qué bloquear el hilo principal lo bloquea todo.
  2. Cada fase tiene su propia cola de callbacks. No hay una única "cola de eventos"; hay varias, y la fase actual determina cuál se vacía.
  3. Dentro de una fase, los callbacks se ejecutan hasta agotar su cola (con un límite de seguridad en algunas fases) y solo entonces se pasa a la siguiente.

  1. Las seis fases, en orden

flowchart TD
    START(["Arranque:<br/>se ejecuta tu script completo"]) --> T

    T["<b>1. timers</b><br/>callbacks de setTimeout<br/>y setInterval vencidos"]
    P["<b>2. pending callbacks</b><br/>callbacks de E/S aplazados<br/>de la iteracion anterior"]
    I["<b>3. idle / prepare</b><br/>uso interno de libuv"]
    POLL["<b>4. poll</b><br/>recoge eventos de E/S nueva<br/>y ejecuta sus callbacks.<br/><i>Aqui es donde se espera</i>"]
    CH["<b>5. check</b><br/>callbacks de setImmediate"]
    CL["<b>6. close callbacks</b><br/>eventos 'close' de sockets,<br/>servidores y streams"]
    FIN{"Quedan<br/>referencias?"}

    T --> P --> I --> POLL --> CH --> CL --> FIN
    FIN -->|"Si"| T
    FIN -->|"No"| END(["El proceso termina"])

Entre cada par de fases —y también entre cada callback individual, en Node 11 y posteriores— se vacían dos colas especiales que no forman parte del diagrama porque no son fases del bucle: la de process.nextTick y la de las microtareas de promesas. Las veremos en el apartado 10, y son la fuente número uno de confusión.

Aquí tienes la vista de conjunto antes de entrar en el detalle:

# Fase Qué callbacks ejecuta ¿La ves a menudo?
1 timers setTimeout y setInterval cuyo plazo ya venció Constantemente
2 pending callbacks Callbacks de operaciones del sistema aplazados (p. ej. ECONNREFUSED de TCP) Rara vez de forma consciente
3 idle, prepare Uso interno de libuv Nunca desde JavaScript
4 poll Callbacks de E/S: fichero leído, petición HTTP recibida, socket con datos Es donde vive un servidor
5 check setImmediate Cuando lo pides explícitamente
6 close callbacks socket.on('close'), servidor.on('close') Al cerrar recursos

  1. Fase 1: timers

En esta fase el bucle mira su montón de temporizadores y ejecuta los callbacks de todos aquellos cuyo plazo ya ha vencido.

El matiz esencial: setTimeout(fn, 100) no significa "ejecuta fn dentro de exactamente 100 ms". Significa "no ejecutes fn antes de que pasen 100 ms". El umbral es un mínimo, no una cita.

// src/laboratorio/timers-precision.js
// Un temporizador es un minimo, no una promesa de puntualidad.

const inicio = Date.now();

setTimeout(() => {
  console.log(`Temporizador de 100 ms disparado a los ${Date.now() - inicio} ms`);
}, 100);

// Un bloqueo sincrono de 300 ms justo despues de programarlo.
const limite = Date.now() + 300;
while (Date.now() < limite) {
  // Trabajo sincrono: el bucle de eventos ni siquiera ha empezado a girar.
}

console.log(`Bloqueo sincrono terminado a los ${Date.now() - inicio} ms`);
Bloqueo sincrono terminado a los 300 ms
Temporizador de 100 ms disparado a los 300 ms

El temporizador estaba vencido desde los 100 ms, pero el bucle no pudo atenderlo hasta que el hilo principal quedó libre. Un temporizador nunca se dispara antes de su plazo, pero puede dispararse muchísimo después. En un servidor saturado, esa diferencia es la señal de que algo va mal.

Nota sobre setInterval: no garantiza una cadencia exacta. Si el callback tarda más que el intervalo, Node no acumula ejecuciones pendientes: descarta las que no cabían y programa la siguiente. Un setInterval(fn, 100) cuyo fn tarda 250 ms acaba corriendo cada 250 ms, no cada 100.

  1. Fase 2: pending callbacks

Esta fase ejecuta callbacks de ciertas operaciones del sistema que quedaron aplazados de la iteración anterior. El caso típico es un error de TCP: cuando intentas conectar a un puerto cerrado, algunos sistemas notifican el ECONNREFUSED de forma que libuv lo encola aquí en lugar de en poll.

Es la fase que menos vas a tocar directamente. Debes saber que existe —porque explica algún orden de ejecución aparentemente raro con errores de red— pero no vas a programar nada en ella. También se la conoce en la documentación antigua como pending i/o callbacks.

  1. Fase 3: idle y prepare

Uso interno de libuv, sin API pública desde JavaScript. Node las utiliza para su propia preparación antes de entrar en poll. Aparecen en todos los diagramas por completitud y para que, cuando leas la documentación oficial, no te preguntes qué te has perdido. Puedes ignorarlas.

  1. Fase 4: poll, el corazón del corazón

Si hay una fase que debes entender de verdad, es esta. Un servidor Node pasa el 99 % de su vida aquí.

La fase poll hace dos cosas:

  1. Calcular cuánto tiempo puede permitirse bloquear esperando eventos de E/S.
  2. Procesar los eventos de E/S que ya han llegado, ejecutando sus callbacks.

Y el algoritmo de decisión, que es lo interesante, es este:

flowchart TD
    A["Entramos en la fase poll"] --> B{"Hay callbacks de E/S<br/>en la cola?"}
    B -->|"Si"| C["Ejecutarlos hasta agotar la cola<br/>(o alcanzar el limite del sistema)"]
    C --> Z["Pasar a la fase check"]
    B -->|"No"| D{"Hay setImmediate<br/>programados?"}
    D -->|"Si"| Z2["Terminar poll YA<br/>y pasar a check"]
    D -->|"No"| E{"Hay temporizadores<br/>proximos a vencer?"}
    E -->|"Si"| F["BLOQUEAR aqui esperando E/S,<br/>como maximo hasta que venza<br/>el temporizador mas proximo"]
    E -->|"No"| G["BLOQUEAR aqui esperando E/S<br/>indefinidamente"]
    F --> Z
    G --> Z

Cuatro consecuencias prácticas de este algoritmo:

  • "Bloquear" aquí es lo que quieres, no un problema. Cuando tu servidor no tiene nada que hacer, se queda dormido en la llamada a epoll_wait del sistema operativo, sin consumir CPU, hasta que llega un paquete de red. Un servidor Node en reposo gasta 0 % de procesador. Es el mismo camarero de la primera lección: no espera de pie mirando la cocina, se sienta hasta que suena la campana.
  • setImmediate tiene prioridad sobre esperar. Si hay un setImmediate pendiente, poll no se duerme: corta y pasa a check. Esto es lo que hace que setImmediate sea fiable dentro de callbacks de E/S.
  • Los temporizadores acotan la espera. Si hay un setTimeout que vence en 50 ms, poll dormirá como mucho 50 ms, para poder volver a la fase timers a tiempo.
  • La cola de poll tiene un límite. Node no procesa infinitos eventos de E/S seguidos: existe un tope para no dejar sin atender al resto de fases. Lo sobrante se atiende en la siguiente iteración.

  1. Fase 5: check y setImmediate

La fase check existe por un único motivo: dar a setImmediate un lugar garantizado justo después de la fase poll.

El nombre de setImmediate es engañoso: no significa "ejecútalo ya", significa "ejecútalo en cuanto termine de procesar la E/S de esta iteración". Es decir: en la fase check de la iteración actual.

// src/laboratorio/check.js
const fs = require('node:fs');

fs.readFile(__filename, () => {
  // Estamos en la fase POLL (este es un callback de E/S).
  console.log('1. Callback de lectura de fichero (fase poll)');

  setImmediate(() => {
    console.log('2. setImmediate (fase check, misma iteracion)');
  });

  setTimeout(() => {
    console.log('3. setTimeout 0 (fase timers, siguiente iteracion)');
  }, 0);
});
1. Callback de lectura de fichero (fase poll)
2. setImmediate (fase check, misma iteracion)
3. setTimeout 0 (fase timers, siguiente iteracion)

Este orden es siempre el mismo, sin excepción. Y ya sabes por qué: desde poll, la siguiente fase es check, mientras que para llegar a timers hay que completar la iteración entera y empezar otra.

  1. Fase 6: close callbacks

Cuando un recurso se cierra —un socket, un servidor, un stream—, su evento 'close' no se emite inmediatamente: se encola en esta última fase.

// src/laboratorio/cierre.js
const net = require('node:net');

const servidor = net.createServer();

servidor.listen(0, () => {   // El puerto 0 significa "uno libre cualquiera"
  console.log(`1. Servidor escuchando en el puerto ${servidor.address().port}`);

  servidor.close();          // Pedimos el cierre...
  console.log('2. close() ya ha vuelto, pero el evento aun no se ha emitido');

  setImmediate(() => console.log('3. setImmediate (fase check)'));
});

servidor.on('close', () => {
  console.log('4. Evento close (fase close callbacks)');
});
1. Servidor escuchando en el puerto 43217
2. close() ya ha vuelto, pero el evento aun no se ha emitido
3. setImmediate (fase check)
4. Evento close (fase close callbacks)

Nota la lección de fondo, que se repetirá en toda tu carrera con Node: pedir que algo se cierre y que algo esté cerrado son dos momentos distintos. En el Módulo 11 esto será importante para apagar el servidor de Escena Viva de forma ordenada sin cortar peticiones en curso.

  1. setTimeout frente a setImmediate

Ahora la pregunta clásica de las entrevistas, que tiene una respuesta con dos partes.

9.1 En el nivel superior del script: no determinista

// src/laboratorio/orden-no-determinista.js

setTimeout(() => console.log('setTimeout 0'), 0);
setImmediate(() => console.log('setImmediate'));

Ejecuta esto cinco o seis veces seguidas:

for i in 1 2 3 4 5; do node src/laboratorio/orden-no-determinista.js; echo '---'; done

Verás que el orden cambia entre ejecuciones:

setTimeout 0
setImmediate
---
setImmediate
setTimeout 0
---
setTimeout 0
setImmediate
---

La explicación: setTimeout(fn, 0) se convierte internamente en un plazo de 1 ms. Cuando el bucle arranca por primera vez y entra en la fase timers, comprueba si ha pasado ese milisegundo. Si el arranque del proceso (cargar módulos, inicializar V8) tardó más de 1 ms, el temporizador ya está vencido y se ejecuta primero; si tardó menos, no está vencido, el bucle sigue hasta check y gana setImmediate. Como ese tiempo de arranque depende de la carga de la máquina, el resultado es una carrera.

Regla práctica: nunca escribas código cuya corrección dependa del orden entre setTimeout y setImmediate en el nivel superior. Si te importa el orden, es que estás usando la herramienta equivocada.

9.2 Dentro de un callback de E/S: totalmente determinista

Como viste en el apartado 7, dentro de un callback de E/S estamos ya en la fase poll, y desde ahí check viene inmediatamente después mientras que timers queda a una iteración de distancia. setImmediate siempre gana. Sin excepciones, sin depender de la máquina.

Contexto Ganador Por qué
Nivel superior del script Impredecible Depende de si ha pasado 1 ms desde el arranque
Dentro de un callback de E/S (fs, net, http) setImmediate, siempre Estamos en poll; check es la fase siguiente
Dentro de un setImmediate setTimeout, normalmente Ya hemos pasado check: el siguiente setImmediate va a la próxima iteración
Dentro de un setTimeout Impredecible Misma carrera del primer caso

9.3 Cuándo usar cada uno

Quiero… Uso
Ceder el control ahora mismo para no bloquear, y seguir cuanto antes setImmediate(fn)
Ejecutar algo dentro de N milisegundos setTimeout(fn, N)
Trocear un trabajo largo en pedazos que no bloqueen setImmediate entre pedazo y pedazo
Ejecutar algo antes de que el bucle continúe, cueste lo que cueste process.nextTick (con cuidado, ver siguiente apartado)

  1. Las dos colas que se cuelan: process.nextTick y las microtareas

Aquí está el 80 % de la confusión que sufre la gente con Node. Existen dos colas que no pertenecen a ninguna fase y que se vacían entre fases, y también entre cada callback individual:

Cola Se llena con Prioridad
nextTick queue process.nextTick(fn) La más alta. Se vacía primero
microtask queue Promise.then/catch/finally, await, queueMicrotask La segunda. Se vacía después de la de nextTick

Y la regla completa, en el orden exacto en que Node actúa:

Después de terminar cada callback, y antes de continuar con el siguiente o de cambiar de fase, Node:

  1. Vacía completamente la cola de nextTick (incluidos los nextTick que se añadan mientras la vacía).
  2. Vacía completamente la cola de microtareas (incluidas las que se añadan mientras la vacía).

Nota histórica importante: este comportamiento cambió en Node.js 11. Antes, las colas se vaciaban solo entre fases, no entre callbacks individuales. Si encuentras artículos antiguos con salidas distintas, es por eso. Todo lo de esta lección se refiere a Node 11 y posteriores, que es todo lo que se usa hoy.

Un ejemplo mínimo que lo demuestra:

// src/laboratorio/colas.js

console.log('1. sincrono');

setTimeout(() => console.log('5. setTimeout'), 0);
setImmediate(() => console.log('6. setImmediate'));

Promise.resolve().then(() => console.log('4. microtarea de promesa'));
process.nextTick(() => console.log('3. nextTick'));

console.log('2. sincrono fin');
1. sincrono
2. sincrono fin
3. nextTick
4. microtarea de promesa
5. setTimeout
6. setImmediate

(Las dos últimas líneas pueden intercambiarse, por lo que ya sabes del apartado 9.1. Las cuatro primeras son inamovibles.)

Y la demostración de que las colas se vacían entre cada callback, no solo entre fases:

// src/laboratorio/colas-entre-callbacks.js

setTimeout(() => {
  console.log('timer 1');
  process.nextTick(() => console.log('  nextTick desde timer 1'));
}, 0);

setTimeout(() => {
  console.log('timer 2');
  process.nextTick(() => console.log('  nextTick desde timer 2'));
}, 0);
timer 1
  nextTick desde timer 1
timer 2
  nextTick desde timer 2

Los dos temporizadores están en la misma fase. Si las colas se vaciaran solo al cambiar de fase, veríamos timer 1, timer 2 y luego los dos nextTick. Que no sea así confirma la regla: se vacían entre callback y callback.

¿Cuándo usar process.nextTick?

Casi nunca, y por eso mismo conviene saber los dos casos legítimos:

  1. Garantizar que un callback sea siempre asíncrono, incluso en la ruta de error inmediato. Lo veremos con detalle en la próxima lección, Callbacks y Programación Asíncrona, porque es su remedio canónico.
  2. Permitir que quien te llama registre oyentes antes de emitir un evento. Es lo que hace internamente Node en varios sitios: si un constructor emitiera un evento de forma síncrona, nadie habría tenido ocasión de suscribirse todavía.
// Patron 2: dar tiempo a registrar el oyente.
const EventEmitter = require('node:events');

class CargadorCatalogo extends EventEmitter {
  constructor() {
    super();
    // MAL: nadie ha podido suscribirse todavia.
    // this.emit('listo');

    // BIEN: se emite despues de que el codigo que nos creo pueda hacer .on()
    process.nextTick(() => this.emit('listo'));
  }
}

const cargador = new CargadorCatalogo();
cargador.on('listo', () => console.log('Catalogo listo'));   // Si funciona

En caso de duda, usa setImmediate en lugar de nextTick. Es más seguro, porque respeta el ciclo del bucle en vez de saltárselo. La razón, en el apartado siguiente.

  1. Una traza completa que debes poder predecir

Este es el examen de la lección. Lee el programa entero, escribe en un papel el orden de salida que esperas y solo después ejecútalo.

// src/laboratorio/traza.js
// Reto: predecir el orden de las 8 letras antes de ejecutar.

console.log('A - sincrono');

setTimeout(() => {
  console.log('B - primer setTimeout');
  process.nextTick(() => console.log('C - nextTick dentro del primer setTimeout'));
  Promise.resolve().then(() => console.log('D - promesa dentro del primer setTimeout'));
}, 0);

setTimeout(() => {
  console.log('E - segundo setTimeout');
}, 0);

process.nextTick(() => console.log('F - nextTick del nivel superior'));

Promise.resolve().then(() => console.log('G - promesa del nivel superior'));

console.log('H - sincrono fin');

La respuesta y su razonamiento:

A - sincrono
H - sincrono fin
F - nextTick del nivel superior
G - promesa del nivel superior
B - primer setTimeout
C - nextTick dentro del primer setTimeout
D - promesa dentro del primer setTimeout
E - segundo setTimeout

Paso a paso:

Paso Qué ocurre Salida
1 Se ejecuta el script de arriba abajo. Los setTimeout, el nextTick y el .then solo encolan, no ejecutan A, H
2 Termina el script. Antes de que el bucle empiece a girar, Node vacía la cola de nextTick F
3 Después, la cola de microtareas G
4 Primera iteración, fase timers. Se ejecuta el primer callback vencido B
5 Ese callback ha terminado: se vacía la cola de nextTick antes de seguir C
6 Y a continuación la de microtareas D
7 Solo ahora se pasa al segundo callback de temporizador, que estaba en la misma fase E
8 Sin más colas ni referencias, el bucle sale y el proceso termina —

Si acertaste el orden completo, entiendes el bucle de eventos. Si fallaste en C y D colocándolos detrás de E, tienes en la cabeza el modelo anterior a Node 11: las colas se vacían entre callbacks, no solo entre fases.

  1. La inanición del bucle: el peligro de nextTick

La cola de nextTick se vacía completamente, incluidos los nextTick que se encolen mientras se está vaciando. Eso abre una puerta a un fallo especialmente desagradable:

// src/laboratorio/inanicion.js
// AVISO: este programa NUNCA llega al setTimeout. Paralo con Ctrl+C.

let vueltas = 0;

function recursivoConNextTick() {
  vueltas++;
  if (vueltas % 1000000 === 0) {
    console.log(`${vueltas} vueltas y el bucle sigue sin avanzar`);
  }
  process.nextTick(recursivoConNextTick);   // Se vuelve a encolar a si mismo
}

setTimeout(() => {
  console.log('Esto NO se imprime jamas');
}, 100);

recursivoConNextTick();

El bucle de eventos nunca llega a la fase timers, porque antes de avanzar tiene que vaciar la cola de nextTick, y esa cola se rellena sola indefinidamente. El proceso consume 100 % de un núcleo, no atiende nada y no da ningún error. A esto se le llama inanición del bucle de eventos (event loop starvation), y es un fallo difícil de diagnosticar porque el proceso parece vivo.

Ahora la misma estructura con setImmediate:

// src/laboratorio/sin-inanicion.js

let vueltas = 0;

function recursivoConImmediate() {
  vueltas++;
  if (vueltas >= 1000000) return;
  setImmediate(recursivoConImmediate);
}

setTimeout(() => {
  console.log(`El setTimeout SI se ejecuta, tras ${vueltas} vueltas`);
}, 100);

recursivoConImmediate();
El setTimeout SI se ejecuta, tras 41732 vueltas

La diferencia es que un setImmediate encolado durante la fase check se ejecuta en la siguiente iteración, no en la actual. Eso deja pasar el resto de fases y el temporizador llega a dispararse.

process.nextTick setImmediate
Cuándo se ejecuta Antes de continuar, saltándose el bucle En la fase check de una iteración del bucle
Recursión Provoca inanición Segura: cede el control cada vuelta
Prioridad Máxima Normal
Uso recomendado Casos muy concretos (ver 10) La opción por defecto para "más tarde, pero pronto"

Esta es la regla que debe quedarte grabada: si necesitas aplazar trabajo, usa setImmediate. process.nextTick es una herramienta afilada para dos casos específicos.

  1. setTimeout(fn, 0) no es inmediato

Ya lo has visto de pasada, pero merece su propio apartado porque genera errores reales.

Cuando escribes setTimeout(fn, 0), Node no programa 0 milisegundos. Internamente aplica esta corrección:

// Comportamiento interno, simplificado
if (!(plazo >= 1 && plazo <= 2147483647)) {
  plazo = 1;   // Cualquier valor fuera del rango se convierte en 1 ms
}

Es decir: el mínimo real es 1 milisegundo. Y hay dos límites más que conviene conocer:

Valor que escribes Lo que ocurre realmente
setTimeout(fn, 0) Plazo de 1 ms
setTimeout(fn, -5) Plazo de 1 ms
setTimeout(fn) (sin plazo) Plazo de 1 ms
setTimeout(fn, 2147483647) ~24,8 días. El máximo (entero de 32 bits con signo)
setTimeout(fn, 2147483648) Desbordamiento: se convierte en 1 ms y Node avisa por consola. Se ejecuta ¡inmediatamente!

Ese último caso es un error real y recurrente: alguien programa "un recordatorio dentro de 30 días" con setTimeout(fn, 30 * 24 * 60 * 60 * 1000) y el callback se dispara al instante, porque 2.592.000.000 supera el máximo. En Escena Viva, un recordatorio de "tu evento es mañana" jamás debe implementarse con un setTimeout largo: se implementa con una tarea programada externa o una cola de trabajos.

Y otra consecuencia práctica: el plazo mínimo real de 1 ms significa que 1000 setTimeout(fn, 0) encadenados tardan al menos un segundo. Para trocear trabajo sin esperar nada, setImmediate es la herramienta correcta y es mucho más rápida.

  1. Medir el retraso del bucle de eventos

Llegamos a la parte más aplicable de la lección. El retraso del bucle de eventos (event loop lag o delay) es la diferencia entre el momento en que un callback debería haberse ejecutado y el momento en que realmente se ejecutó.

Es la métrica de salud número uno de un servidor Node, por encima incluso de la CPU y la memoria, y la razón es directa: el retraso del bucle es, literalmente, el tiempo que tus usuarios están esperando por culpa de que el proceso está ocupado.

14.1 La medición manual

// src/utiles/medir-bucle.js
// Mide el retraso del bucle de eventos con un temporizador de referencia.

const INTERVALO_MS = 100;

function iniciarMedicion({ intervaloMs = INTERVALO_MS, umbralMs = 50 } = {}) {
  // hrtime.bigint() da nanosegundos y no se ve afectado por cambios de hora.
  let ultimaMarca = process.hrtime.bigint();

  const temporizador = setInterval(() => {
    const ahora = process.hrtime.bigint();

    // Tiempo real transcurrido, en milisegundos.
    const transcurridoMs = Number(ahora - ultimaMarca) / 1e6;
    ultimaMarca = ahora;

    // El retraso es el exceso sobre el intervalo previsto.
    const retrasoMs = Math.max(0, transcurridoMs - intervaloMs);

    if (retrasoMs > umbralMs) {
      console.error(`[bucle] retraso de ${retrasoMs.toFixed(1)} ms`);
    }
  }, intervaloMs);

  // Que la medicion no impida que el proceso termine.
  temporizador.unref();

  return () => clearInterval(temporizador);
}

module.exports = { iniciarMedicion };

Este fichero ya es, técnicamente, un módulo con module.exports. En la lección Módulos CommonJS y require() formalizaremos qué significa exactamente esa línea; de momento acepta que expone la función para que otros ficheros la usen.

14.2 La medición precisa: perf_hooks

Node trae una herramienta específica y mucho más exacta, porque mide dentro de libuv en lugar de con un temporizador de JavaScript:

// src/laboratorio/monitor-bucle.js
const { monitorEventLoopDelay } = require('node:perf_hooks');

// resolution: cada cuantos ms toma una muestra.
const histograma = monitorEventLoopDelay({ resolution: 10 });
histograma.enable();

// Simulamos carga: un bloqueo de 200 ms en mitad del proceso.
setTimeout(() => {
  const limite = Date.now() + 200;
  while (Date.now() < limite) { /* bloqueo deliberado */ }
}, 300);

setTimeout(() => {
  histograma.disable();

  // Los valores vienen en nanosegundos: dividimos entre 1e6 para tener ms.
  const aMs = (n) => (n / 1e6).toFixed(2);

  console.log('Retraso del bucle de eventos:');
  console.log(`  minimo    : ${aMs(histograma.min)} ms`);
  console.log(`  media     : ${aMs(histograma.mean)} ms`);
  console.log(`  maximo    : ${aMs(histograma.max)} ms`);
  console.log(`  percentil 50: ${aMs(histograma.percentile(50))} ms`);
  console.log(`  percentil 99: ${aMs(histograma.percentile(99))} ms`);
}, 1000);
Retraso del bucle de eventos:
  minimo    : 9.99 ms
  media     : 15.42 ms
  maximo    : 208.67 ms
  percentil 50: 10.21 ms
  percentil 99: 208.67 ms

Cómo leer esos números:

Percentil 99 del retraso Diagnóstico
< 10 ms Sano. El proceso responde con holgura
10 – 50 ms Aceptable, pero hay algo que ocupa el hilo. Merece investigarse
50 – 200 ms Malo. Los usuarios lo notan en cada petición
> 200 ms Crítico. Hay trabajo síncrono pesado. El servidor está funcionalmente caído a ratos

Fíjate en algo importante del ejemplo: la media es de 15 ms, un número tranquilizador. El percentil 99 es de 208 ms, un desastre. Por eso en producción se vigilan percentiles y no medias: la media esconde exactamente los problemas que importan. En el Módulo 11 publicaremos esta métrica de forma continua para el servidor de Escena Viva.

  1. Escena Viva: por qué un cálculo síncrono degrada a todos

Aterricemos todo lo anterior en el proyecto. Imagina que en el panel del administrador de Escena Viva añadimos una vista con la ocupación global de la temporada: 3.000 sesiones a lo largo del año, con su porcentaje, su recaudación y su comparación con la temporada anterior.

La implementación ingenua es esta:

// src/laboratorio/ocupacion-sincrona.js
// Version INGENUA: recalcula todo dentro de la peticion, de forma sincrona.

function calcularOcupacionGlobal(sesiones) {
  const porSala = new Map();

  for (const sesion of sesiones) {
    // Trabajo real por sesion: agregacion, formateo, comparativas...
    const clave = sesion.sala;
    const acumulado = porSala.get(clave) ?? { aforo: 0, vendidas: 0, recaudacionCentimos: 0 };

    acumulado.aforo += sesion.aforo;
    acumulado.vendidas += sesion.vendidas;
    acumulado.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;

    porSala.set(clave, acumulado);
  }

  return porSala;
}

Si esa función tarda 300 ms, y un administrador refresca su panel, ocurre lo siguiente durante esos 300 ms:

  • Todas las peticiones de compra de entradas se detienen. No se ralentizan: se detienen.
  • El retraso del bucle sube a 300 ms. El percentil 99 de todos los usuarios se dispara.
  • Si el servidor atiende 200 peticiones por segundo, 60 personas quedan esperando por un panel que ni siquiera están mirando.
  • Y si el administrador tiene el panel con auto-refresco cada 5 segundos, el servidor pasa el 6 % de su vida congelado, de forma permanente.

Lo grave es que esto no aparece en desarrollo. En tu portátil, con un solo usuario, 300 ms es una página que carga "un poco lenta". En producción, con tráfico, es una caída parcial que ningún registro de errores va a mostrarte, porque técnicamente nada ha fallado.

Las tres soluciones, en orden de sencillez:

Solución Qué hace Dónde se ve
Trocear con setImmediate Procesa 200 sesiones, cede el control, continúa. El retraso máximo baja a ~20 ms Aquí mismo, apartado siguiente
Calcular fuera y cachear El informe se calcula cada 5 minutos en segundo plano; la petición solo lee un valor ya hecho Módulo 10
Mover el cálculo a otro hilo o proceso worker_threads o cluster: el cálculo ocurre fuera del hilo que atiende peticiones Módulo 10

Y así queda la versión troceada, que ya puedes escribir con lo que sabes hoy:

// src/laboratorio/ocupacion-troceada.js
// Version COOPERATIVA: cede el control al bucle cada TAMANO_LOTE sesiones.

const TAMANO_LOTE = 200;

function calcularOcupacionGlobalTroceado(sesiones, alTerminar) {
  const porSala = new Map();
  let indice = 0;

  function procesarLote() {
    const fin = Math.min(indice + TAMANO_LOTE, sesiones.length);

    for (; indice < fin; indice++) {
      const sesion = sesiones[indice];
      const acumulado = porSala.get(sesion.sala) ??
        { aforo: 0, vendidas: 0, recaudacionCentimos: 0 };

      acumulado.aforo += sesion.aforo;
      acumulado.vendidas += sesion.vendidas;
      acumulado.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;

      porSala.set(sesion.sala, acumulado);
    }

    if (indice < sesiones.length) {
      // Cedemos el control: el bucle atiende peticiones antes de seguir.
      setImmediate(procesarLote);
    } else {
      alTerminar(porSala);
    }
  }

  procesarLote();
}

El tiempo total es prácticamente el mismo, incluso algo peor. Pero el tiempo de bloqueo continuo pasa de 300 ms a unos 20 ms, y entre lote y lote el servidor atiende compras con normalidad. Es un intercambio deliberado: sacrificas un poco de rendimiento del informe a cambio de no penalizar a nadie más.

Ese alTerminar(porSala) que ves al final es un callback, y su forma no es casual: es el patrón que estructura toda la asincronía clásica de Node y el tema de la lección siguiente.

Errores Comunes y Consejos

Error 1: creer que setTimeout(fn, 100) ejecuta a los 100 ms exactos. Ejecuta como pronto a los 100 ms. Si el hilo está ocupado, se retrasa lo que haga falta.

Error 2: apoyarse en el orden entre setTimeout(fn, 0) y setImmediate en el nivel superior. Es una carrera y cambia entre ejecuciones. Dentro de un callback de E/S sí es determinista.

Error 3: usar process.nextTick para "aplazar un poco" trabajo. Se salta el bucle entero. En recursión provoca inanición y el proceso deja de responder sin dar ningún error. Usa setImmediate.

Error 4: programar plazos largos con setTimeout. Por encima de 2.147.483.647 ms (~24,8 días) desborda y se ejecuta inmediatamente. Los recordatorios de Escena Viva irán a una cola de trabajos, no a un temporizador.

Error 5: vigilar la media del retraso del bucle en lugar del percentil 99. La media oculta los picos, y los picos son justo el problema.

Error 6: pensar que trocear con setImmediate resuelve el cálculo intensivo. Lo hace tolerable, no lo resuelve. Si el cálculo es realmente pesado, sacarlo del hilo (Módulo 10) es la respuesta.

Error 7: creer que las promesas "van en otro hilo". No. Una promesa se resuelve en la cola de microtareas, en el mismo hilo principal. await no paraleliza nada por sí solo.

Consejo 1: aprende la traza del apartado 11 de memoria. Si puedes predecir esas ocho letras, puedes predecir cualquier programa asíncrono en Node.

Consejo 2: cuando algo "ocurra en el orden equivocado", dibuja las fases. En el 95 % de los casos la respuesta es que estabas mezclando fases distintas del bucle.

Consejo 3: instala la medición del retraso desde el primer día. Es diez líneas de código y detecta problemas que ninguna otra métrica muestra.

Ejercicios

Ejercicio 1: predecir la traza

Sin ejecutar nada, escribe el orden exacto de la salida de este programa y justifica cada línea indicando en qué fase o cola se ejecuta.

const fs = require('node:fs');

console.log('1');

setTimeout(() => {
  console.log('2');
  setImmediate(() => console.log('3'));
  process.nextTick(() => console.log('4'));
}, 0);

setImmediate(() => {
  console.log('5');
  process.nextTick(() => console.log('6'));
});

fs.readFile(__filename, () => {
  console.log('7');
  setTimeout(() => console.log('8'), 0);
  setImmediate(() => console.log('9'));
});

Promise.resolve().then(() => console.log('10'));
process.nextTick(() => console.log('11'));

console.log('12');

Indica también cuál es la única pareja de líneas cuyo orden relativo no está garantizado.

Ejercicio 2: detectar el bloqueo en Escena Viva

Escribe src/laboratorio/vigilante-bucle.js que:

  1. Use monitorEventLoopDelay de node:perf_hooks para medir el retraso.
  2. Simule tráfico con un setInterval de 20 ms que cuente peticiones atendidas.
  3. Ejecute, a los 500 ms, un cálculo síncrono sobre 3.000 sesiones de Escena Viva que tarde en torno a 400 ms.
  4. A los 2 segundos, imprima: peticiones atendidas frente a peticiones esperadas, y el histograma completo (mínimo, media, máximo, p50, p99).
  5. Devuelva process.exitCode = 1 si el percentil 99 supera los 50 ms, con un mensaje por stderr explicando el diagnóstico.

Ejercicio 3: trocear el informe de ocupación

Partiendo de calcularOcupacionGlobalTroceado del apartado 15, escribe src/laboratorio/comparar-troceado.js que ejecute las dos versiones (síncrona y troceada) sobre las mismas 3.000 sesiones mientras un setInterval de 20 ms mide el tráfico atendido, y produzca una tabla comparativa con:

  • Tiempo total de cálculo de cada versión.
  • Bloqueo continuo máximo de cada versión.
  • Peticiones atendidas durante cada versión.

Después, responde por escrito: ¿qué tamaño de lote elegirías para Escena Viva y por qué? ¿Qué pasa si lo pones a 1? ¿Y a 3000?

Soluciones

Solución 1

1     sincrono
12    sincrono
11    cola de nextTick, tras terminar el script
10    cola de microtareas, tras la de nextTick
2     fase timers, primera iteracion
4     cola de nextTick, al terminar el callback del timer
5     fase check, primera iteracion
6     cola de nextTick, al terminar el callback de setImmediate
3     fase check, SEGUNDA iteracion (se encolo estando ya en la primera)
7     fase poll (callback de fs.readFile)
9     fase check, misma iteracion que 7
8     fase timers, siguiente iteracion

Justificación de los tramos que suelen fallarse:

  • 11 antes que 10: la cola de nextTick tiene prioridad sobre la de microtareas de promesa. Siempre.
  • 4 justo después de 2: al terminar el callback del temporizador se vacían las colas antes de continuar. Es el comportamiento de Node 11+.
  • 3 después de 5 y 6: cuando se ejecuta el callback del temporizador (fase timers), programar un setImmediate lo coloca en la fase check de esa misma iteración… pero el setImmediate del nivel superior (5) ya estaba encolado antes, así que va primero. Y 3 se encoló durante la iteración actual mientras ya estábamos camino de check: dependiendo del momento exacto puede entrar en la misma cola de check o en la siguiente. En la práctica, 5 siempre precede a 3.
  • 7 al final del grupo: fs.readFile es E/S real. Tarda más que todo lo anterior, así que su callback llega en una iteración posterior.
  • 9 antes que 8: dentro de un callback de E/S (fase poll), setImmediate (check, fase siguiente) siempre gana a setTimeout (timers, iteración siguiente). Este es el orden determinista del apartado 9.2.

La única pareja no garantizada en el conjunto es la posición relativa de 7 respecto al bloque 2/4/5/6/3: depende de lo que tarde el disco en devolver el fichero. En una máquina con el fichero en caché puede llegar antes de lo que muestra la traza. Todo lo demás es fijo.

Solución 2

// src/laboratorio/vigilante-bucle.js
// Mide el impacto de un calculo sincrono sobre el trafico de Escena Viva.

const { monitorEventLoopDelay } = require('node:perf_hooks');

const INTERVALO_TRAFICO_MS = 20;
const DURACION_MS = 2000;
const UMBRAL_P99_MS = 50;

const histograma = monitorEventLoopDelay({ resolution: 5 });
histograma.enable();

const inicio = Date.now();
let atendidas = 0;

const trafico = setInterval(() => {
  atendidas++;
}, INTERVALO_TRAFICO_MS);

// Catalogo de temporada completa: 3000 sesiones.
const sesiones = [];
for (let i = 1; i <= 3000; i++) {
  sesiones.push({
    id: `ses-${String(Math.ceil(i / 3)).padStart(3, '0')}-${(i % 3) + 1}`,
    sala: ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'][i % 3],
    aforo: 420,
    vendidas: i % 420,
    precioCentimos: 2500
  });
}

// Calculo sincrono deliberadamente caro (~400 ms).
function calcularOcupacionGlobal(sesiones) {
  const porSala = new Map();
  for (const sesion of sesiones) {
    let ruido = 0;
    for (let i = 0; i < 60000; i++) {
      ruido += Math.sqrt(i);        // Trabajo artificial.
    }
    const acumulado = porSala.get(sesion.sala) ??
      { aforo: 0, vendidas: 0, recaudacionCentimos: 0 };
    acumulado.aforo += sesion.aforo;
    acumulado.vendidas += sesion.vendidas;
    acumulado.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;
    porSala.set(sesion.sala, acumulado);
  }
  return porSala;
}

setTimeout(() => {
  const t0 = Date.now();
  calcularOcupacionGlobal(sesiones);
  console.error(`[informe] calculo sincrono de ${Date.now() - t0} ms`);
}, 500);

setTimeout(() => {
  clearInterval(trafico);
  histograma.disable();

  const aMs = (n) => (n / 1e6).toFixed(2);
  const esperadas = Math.floor((Date.now() - inicio) / INTERVALO_TRAFICO_MS);
  const p99 = histograma.percentile(99) / 1e6;

  console.log('');
  console.log('TRAFICO');
  console.log(`  Peticiones esperadas : ${esperadas}`);
  console.log(`  Peticiones atendidas : ${atendidas}`);
  console.log(`  Perdidas             : ${esperadas - atendidas}`);
  console.log('');
  console.log('RETRASO DEL BUCLE DE EVENTOS');
  console.log(`  minimo : ${aMs(histograma.min)} ms`);
  console.log(`  media  : ${aMs(histograma.mean)} ms`);
  console.log(`  maximo : ${aMs(histograma.max)} ms`);
  console.log(`  p50    : ${aMs(histograma.percentile(50))} ms`);
  console.log(`  p99    : ${aMs(histograma.percentile(99))} ms`);

  if (p99 > UMBRAL_P99_MS) {
    console.error('');
    console.error(
      `DIAGNOSTICO: el percentil 99 (${p99.toFixed(1)} ms) supera el umbral de ` +
      `${UMBRAL_P99_MS} ms. Hay trabajo sincrono bloqueando el hilo principal. ` +
      'Trocealo con setImmediate, cachea el resultado o muevelo a un worker.'
    );
    process.exitCode = 1;
  }
}, DURACION_MS);

Salida típica:

[informe] calculo sincrono de 412 ms

TRAFICO
  Peticiones esperadas : 100
  Peticiones atendidas : 80
  Perdidas             : 20

RETRASO DEL BUCLE DE EVENTOS
  minimo : 4.98 ms
  media  : 12.31 ms
  maximo : 414.20 ms
  p50    : 5.12 ms
  p99    : 414.20 ms

DIAGNOSTICO: el percentil 99 (414.2 ms) supera el umbral de 50 ms. ...

Fíjate una vez más en la disociación entre media (12 ms, aparentemente sana) y percentil 99 (414 ms, desastroso). Es exactamente lo que verías en un panel de producción mal configurado que solo muestre medias.

Solución 3

// src/laboratorio/comparar-troceado.js
// Compara el calculo sincrono con el troceado, midiendo el trafico atendido.

const TAMANO_LOTE = Number(process.argv[2]) || 200;
const INTERVALO_TRAFICO_MS = 20;

const sesiones = [];
for (let i = 1; i <= 3000; i++) {
  sesiones.push({
    sala: ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'][i % 3],
    aforo: 420,
    vendidas: i % 420,
    precioCentimos: 2500
  });
}

// Trabajo por sesion, identico en ambas versiones.
function acumular(porSala, sesion) {
  let ruido = 0;
  for (let i = 0; i < 60000; i++) {
    ruido += Math.sqrt(i);
  }
  const a = porSala.get(sesion.sala) ?? { aforo: 0, vendidas: 0, recaudacionCentimos: 0 };
  a.aforo += sesion.aforo;
  a.vendidas += sesion.vendidas;
  a.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;
  porSala.set(sesion.sala, a);
}

function version1Sincrona() {
  const porSala = new Map();
  for (const sesion of sesiones) acumular(porSala, sesion);
  return porSala;
}

function version2Troceada(alTerminar) {
  const porSala = new Map();
  let indice = 0;
  let bloqueoMaximo = 0;

  function procesarLote() {
    const t0 = Date.now();
    const fin = Math.min(indice + TAMANO_LOTE, sesiones.length);
    for (; indice < fin; indice++) acumular(porSala, sesiones[indice]);
    bloqueoMaximo = Math.max(bloqueoMaximo, Date.now() - t0);

    if (indice < sesiones.length) setImmediate(procesarLote);
    else alTerminar(porSala, bloqueoMaximo);
  }

  procesarLote();
}

// --- Medicion ---
let atendidas = 0;
const trafico = setInterval(() => { atendidas++; }, INTERVALO_TRAFICO_MS);

const resultados = [];

// Version 1
setTimeout(() => {
  const base = atendidas;
  const t0 = Date.now();
  version1Sincrona();
  const total = Date.now() - t0;
  resultados.push({ version: 'sincrona', totalMs: total, bloqueoMaxMs: total, atendidas: atendidas - base });

  // Version 2, en cuanto termina la primera.
  const base2 = atendidas;
  const t1 = Date.now();
  version2Troceada((_, bloqueoMaximo) => {
    resultados.push({
      version: `troceada (lote ${TAMANO_LOTE})`,
      totalMs: Date.now() - t1,
      bloqueoMaxMs: bloqueoMaximo,
      atendidas: atendidas - base2
    });

    clearInterval(trafico);
    console.table(resultados);
  });
}, 200);

Resultado típico con lote de 200:

┌─────────┬──────────────────────┬─────────┬──────────────┬───────────┐
│ (index) │ version              │ totalMs │ bloqueoMaxMs │ atendidas │
├─────────┼──────────────────────┼─────────┼──────────────┼───────────┤
│ 0       │ 'sincrona'           │ 408     │ 408          │ 0         │
│ 1       │ 'troceada (lote 200)'│ 431     │ 29           │ 21        │
└─────────┴──────────────────────┴─────────┴──────────────┴───────────┘

Análisis:

  • El tiempo total empeora un 6 % (408 → 431 ms). Ese es el precio de ceder el control quince veces.
  • El bloqueo continuo máximo cae de 408 ms a 29 ms: una mejora de catorce veces en lo que de verdad afecta a los usuarios.
  • Las peticiones atendidas pasan de 0 a 21. En la versión síncrona no se atendió absolutamente nada.

Respuesta razonada sobre el tamaño de lote:

  • Lote de 1: el bloqueo máximo sería mínimo (~0,2 ms), pero se harían 3.000 vueltas del bucle. El tiempo total se dispararía por el coste de programar y despachar 3.000 setImmediate. Mucho trabajo administrativo para muy poca ganancia adicional.
  • Lote de 3000: es exactamente la versión síncrona, con el coste extra de la maquinaria de troceado. Lo peor de ambos mundos.
  • Elección para Escena Viva: un lote calibrado para que cada trozo dure entre 5 y 20 ms. No se elige por número de elementos, sino por tiempo objetivo, porque el coste por sesión puede cambiar. Un enfoque más robusto es un lote adaptativo: medir cuánto tardó el lote anterior y ajustar el tamaño para acercarse a los 10 ms.

Y la conclusión honesta: trocear es una tirita, muy útil y aplicable hoy mismo, pero la solución definitiva para el panel de ocupación de Escena Viva es no calcularlo dentro de la petición — cachear el resultado o moverlo a un worker, ambas cosas en el Módulo 10.

Conclusión

Has recorrido el corazón de Node.js. Ahora sabes que el bucle de eventos es un while en C dentro de libuv que gira mientras queden referencias vivas, y que cada vuelta atraviesa seis fases en orden fijo: timers para los setTimeout vencidos, pending callbacks para callbacks de sistema aplazados, idle/prepare de uso interno, poll —donde un servidor pasa su vida, esperando E/S sin gastar CPU—, check para setImmediate, y close callbacks para los eventos de cierre.

Has entendido las decisiones de la fase poll: bloquea esperando eventos, pero corta inmediatamente si hay un setImmediate pendiente y acota su espera si hay un temporizador próximo. De ahí sale la respuesta completa a la pregunta clásica: setTimeout(fn, 0) contra setImmediate es una carrera impredecible en el nivel superior, pero dentro de un callback de E/S setImmediate gana siempre.

Has visto las dos colas que no son fases y que se cuelan entre cada callback desde Node 11: la de process.nextTick, con la máxima prioridad, y la de las microtareas de promesas justo detrás. Y has ejecutado la traza de las ocho letras hasta poder predecirla. Sabes también que nextTick recursivo provoca inanición del bucle —un proceso que consume un núcleo entero y no responde sin dar un solo error— y que la opción segura para aplazar trabajo es setImmediate. Y sabes que setTimeout(fn, 0) es en realidad 1 ms, y que por encima de 24,8 días desborda y se ejecuta al instante.

Sobre todo, tienes una herramienta de diagnóstico real: el retraso del bucle de eventos, medido con monitorEventLoopDelay, vigilado por percentil 99 y no por media. Y has comprobado en Escena Viva que un informe síncrono de 400 ms no ralentiza una petición: detiene el servidor entero, dejando sin atender a decenas de personas que solo querían comprar una entrada. Trocear con setImmediate bajó el bloqueo de 408 ms a 29 ms; la solución definitiva —cachear o mover a otro hilo— espera en el Módulo 10.

Todo este mecanismo existe para servir a una forma concreta de escribir código: entregar a Node una función que se llamará cuando el trabajo esté listo. Esa función tiene un nombre y una forma canónica en Node, con el error siempre en primer lugar, y unas reglas que si se rompen producen errores desconcertantes. En la siguiente lección, Callbacks y Programación Asíncrona, escribirás tus propias funciones asíncronas para Escena Viva, descubrirás por qué try/catch deja de funcionar, y construirás —a propósito— la pirámide de la muerte que las promesas vendrán a derribar.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados