Menu

Event loop en Node.js: fases, nextTick y setImmediate

El event loop de Node.js ejecuta tus callbacks por fases (timers, poll, check, close), y entre cada callback vacía la cola de process.nextTick y después la de promesas. Ejecuta los ejemplos para ver el orden exacto.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

El event loop de Node.js es el bucle que decide qué callback se ejecuta a continuación una vez que tu código síncrono ha terminado. Recorre unas fases en un ciclo fijo: timers (setTimeout, setInterval), pending callbacks, poll (I/O como lecturas de archivos y datos de red), check (setImmediate) y close callbacks. Después de cada callback, Node vacía primero la cola de process.nextTick y luego la de promesas, así que estas siempre se ejecutan antes de que el loop avance.

Imprime sync, nextTick, promise, setTimeout, justo al revés de como se escribieron. El código síncrono siempre termina primero, luego Node vacía la cola de nextTick, después la de promesas, y solo entonces arranca el loop y llega a la fase de timers.

Las fases del event loop

El loop de Node viene de libuv, la librería en C que Node usa para el I/O. Antes de la primera vuelta ejecuta los timers que ya han vencido, y después cada vuelta ejecuta estas fases en orden:

  1. Pending callbacks: algunos callbacks del sistema aplazados de la vuelta anterior, como ciertos errores de red.
  2. Poll: los nuevos eventos de I/O y sus callbacks (terminó la lectura de un archivo, llegaron datos a un socket). Cuando no hay nada más programado, el loop espera aquí.
  3. Check: los callbacks de setImmediate.
  4. Close callbacks: los eventos 'close', como un socket que se destruyó.
  5. Timers: los callbacks de setTimeout y setInterval a los que les ha llegado la hora.

Muchas guías ponen los timers al principio de cada vuelta, que es como funcionaba Node antes de la versión 20 (libuv 1.45). Los callbacks se ejecutan en el mismo orden de las dos formas: a los timers de una vuelta les sigue el poll de la vuelta siguiente.

Hay dos colas fuera de las fases: la cola de nextTick y la de microtareas (callbacks de promesas y queueMicrotask). Node vacía las dos después de cada callback, en cualquier fase, primero nextTick. El loop sigue girando mientras haya algo que esperar (un timer, un archivo o socket abierto, un servidor) y el proceso termina cuando no queda nada.

Este es el loop propio de Node. El navegador comparte la idea de tareas y microtareas, pero no tiene fases, ni setImmediate, ni process.nextTick; la página del event loop de JavaScript cubre la visión a nivel de lenguaje.

process.nextTick vs promesas

Las dos se ejecutan antes de que el loop continúe, pero no se mezclan. Node ejecuta toda la cola de nextTick, incluidos los ticks que se añaden mientras se ejecuta, y solo después la cola de promesas. Intenta predecir esta:

El orden es tick 1, tick 2, tick from tick, promise 1, promise 2. El tick programado dentro de otro tick se une a la misma cola y aun así se ejecuta antes que cualquier promesa, aunque la promesa se creara antes.

En el nivel superior de un ES module (un archivo que usa import) las dos se intercambian: el propio código del módulo se ejecuta dentro de un job de promesa, así que sus callbacks de promesas terminan antes de que Node llegue a la cola de nextTick. Dentro de callbacks, la regla de nextTick primero se cumple en los dos sistemas de módulos.

setImmediate vs setTimeout(fn, 0)

setTimeout(fn, 0) en realidad significa "dentro de al menos 1 ms, en la fase de timers". setImmediate(fn) significa "en la próxima fase check". Cuál se ejecuta primero depende de dónde los llames.

Dentro de un callback de I/O el orden es fijo. El callback se ejecuta en la fase poll, la fase check viene justo después, y la fase de timers no llega hasta después:

Imprime nextTick, setImmediate, setTimeout, siempre. Eso hace de setImmediate la opción correcta para "ejecuta esto después del trabajo de I/O actual".

En el módulo principal, fuera de cualquier callback, el orden de los dos no es fijo:

// Order depends on how fast the process started
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));

Si ha pasado al menos 1 ms cuando arranca el loop, el timer ya toca y se imprime primero; si no, gana setImmediate. No escribas código que dependa de ninguno de los dos resultados.

Se ejecutaÚsalo para
process.nextTick(fn)Justo después de la operación actual, antes de las promesasEmitir un evento o un error después de que quien llama haya añadido sus listeners
Promise.then, queueMicrotaskCuando la cola de nextTick está vacíaCódigo asíncrono normal
setImmediate(fn)Fase check, después de los callbacks de I/OCeder el turno al loop entre trozos de trabajo
setTimeout(fn, 0)Fase de timers, al menos 1 ms despuésRetrasos en los que el momento exacto no importa

Bloquear el event loop

Como hay un solo hilo de JavaScript, un callback que tarda mucho lo detiene todo: timers, callbacks de I/O y, en un servidor, todas las demás peticiones. El retraso de un timer es un mínimo, no una garantía.

Primero se imprime loop finished y luego true: el timer tocaba a los 10 ms, pero no pudo ejecutarse hasta que el bucle ocupado devolvió el hilo. En un servidor, ese mismo bucle dentro de un route handler congela todas las demás peticiones durante medio segundo.

La solución para el trabajo largo es dividirlo y darle un turno al loop entre los trozos con setImmediate:

El timer se cuela después de la parte 1. Sustituye setImmediate(() => work(part + 1)) por work(part + 1) y vuelve a ejecutarlo: las tres partes se ejecutan de golpe y timer se imprime al final.

Para un trabajo pesado de principio a fin, como procesar imágenes, dividirlo no basta: sácalo del hilo principal con worker_threads.

Errores comunes

Llamar a process.nextTick de forma recursiva. Un callback de nextTick que no para de programar otro nextTick nunca deja continuar al loop, así que los timers y el I/O se quedan sin turno. Usa setImmediate para trabajo repetido.

Usar APIs síncronas en un servidor. fs.readFileSync, crypto.pbkdf2Sync y las llamadas grandes a JSON.parse bloquean el loop para todos los usuarios. Usa las versiones asíncronas dentro de los request handlers.

Esperar que setTimeout(fn, 0) se ejecute de inmediato. Se ejecuta después de todo el código síncrono, de cada nextTick y de cada callback de promesa, y solo cuando el loop llega a la fase de timers.

Preguntas frecuentes

¿Node.js es single-threaded?

Tu JavaScript se ejecuta en un solo hilo, así que dos callbacks nunca se ejecutan a la vez. Node en sí usa más hilos: libuv mantiene un pool (cuatro hilos por defecto) para el sistema de archivos, las consultas DNS y parte de crypto, y devuelve los resultados al loop.

¿Cuándo termina un programa de Node.js?

Cuando el event loop ya no tiene nada que esperar: ni timers, ni I/O pendiente, ni servidores o sockets abiertos. Un setInterval activo o un servidor escuchando mantienen vivo el proceso hasta que lo limpias o lo cierras.

¿El event loop de Node.js es el mismo que el del navegador?

La idea es la misma, un hilo que ejecuta callbacks en cola, pero los detalles cambian. El navegador no tiene setImmediate ni process.nextTick y encaja el renderizado entre tareas, mientras que Node ejecuta las fases de libuv. La página del event loop de JavaScript cubre la parte común, la del lenguaje.

¿Cómo sé si el event loop está bloqueado?

Mide cuánto se retrasa: monitorEventLoopDelay() de perf_hooks registra el retraso, y una media de decenas de milisegundos significa que algún callback ocupa el hilo demasiado tiempo. Ejecuta Node con --cpu-prof para ver qué función es.

Ilustración de los lenguajes de programación de Coddy

Aprende a programar con Coddy

Comenzar