El bucle de eventos es el mecanismo que hace que toda la historia de concurrencia de Node funcione: un bucle, impulsado por libuv, que decide qué parte de tu JavaScript pendiente se ejecuta a continuación. Es lo que permite que un lenguaje de un solo hilo maneje miles de conexiones concurrentes, y lo que convierte una función síncrona descuidada en una interrupción del servicio.
Cómo funciona Node.js cubre la imagen más amplia de V8 más libuv; esta página profundiza un nivel más en el bucle en sí: sus fases, sus reglas de programación y las suposiciones que lo hacen rápido cuando se respetan y peligroso cuando se ignoran. Trata esto como el ancla conceptual de la sección: los ejemplos prácticos en Conceptos básicos del bucle de eventos y las inmersiones profundas fase por fase y de microtareas que siguen se basan en el modelo descrito aquí.
El bucle de eventos es un ciclo fijo de fases que libuv ejecuta repetidamente, cada fase responsable de una categoría de devolución de llamada pendiente, con la pila de llamadas requerida para estar vacía antes de que el bucle avance.
Por qué es importante: Casi todas las preguntas de "por qué esto se ejecutó antes que aquello" o "por qué mi API se detuvo" en Node se remontan a cómo este ciclo realmente ordena el trabajo; adivinar te da la respuesta incorrecta con más frecuencia de lo que no.
Conceptos clave:pila de llamadas, ejecución hasta la finalización, fase, macrotarea, microtarea, programación cooperativa.
Cuándo usar este modelo: Diagnosticar errores de orden, razonar sobre la latencia bajo carga, decidir si el trabajo pertenece al hilo principal y leer las páginas más detalladas de fase/microtarea con el marco correcto ya establecido.
Limitaciones / Compromisos: El bucle garantiza un orden relativo, no una sincronización exacta, y es cooperativo, lo que significa que confía en que cada devolución de llamada termine rápidamente. No tiene forma de anticipar una devolución de llamada que no lo hace.
Temas relacionados: Fases de libuv, microtareas vs. macrotareas, temporizadores y programación, detección de bloqueo del bucle de eventos.
JavaScript en Node sigue la ejecución hasta la finalización: una vez que una función comienza a ejecutarse, se ejecuta hasta que regresa, y nada más, ninguna otra devolución de llamada, ninguna finalización de E/S, nada, puede interrumpirla. Todo el trabajo del bucle de eventos es decidir qué se ejecuta a continuación, y solo puede tomar esa decisión cuando la pila de llamadas está completamente vacía.
console.log('a');setTimeout(() => console.log('b'), 0);console.log('c');// a, c, b - "b" no puede ejecutarse hasta que el código síncrono anterior termine
Un modelo mental útil: imagina un guardia de seguridad de turno nocturno haciendo rondas fijas por un edificio. Cada ronda visita las mismas estaciones en el mismo orden: revisar los temporizadores de la puerta principal, despejar la sala de correo, escuchar los monitores para detectar nueva actividad, buscar cualquier cosa marcada como urgente, cerrar cualquier cosa que se esté cerrando, y luego el guardia comienza la ronda de nuevo desde el principio. El guardia nunca hace dos estaciones a la vez, y nunca se salta; si algo urgente sucede entre rondas, espera a que el guardia llegue a esa estación. Ese circuito fijo y repetitivo es el bucle de eventos: cada "estación" es una fase, y el guardia es el único hilo de JavaScript que ejecuta lo que esa fase le entrega.
El bucle no es una infraestructura opcional que se encuentra junto a tu código, es lo que mantiene vivo el proceso de Node. Un proceso sale una vez que no hay nada más que hacer: no hay temporizadores pendientes, no hay sockets de servidor abiertos, no hay E/S en cola. Cada server.listen() o manejador abierto es efectivamente una razón para que el guardia siga haciendo rondas; llama a .unref() en un manejador para decirle al bucle "no te quedes vivo solo por esto".
Cada pasada por el bucle visita las mismas fases en el mismo orden fijo:
┌───────────────┐
│ timers │ callbacks de setTimeout / setInterval cuyo tiempo ha pasado
├───────────────┤
│ pending cbs │ algunas callbacks a nivel de sistema diferidas de la pasada anterior
├───────────────┤
│ idle, prepare │ solo para uso interno
├───────────────┤
│ poll │ recuperar nuevos eventos de E/S; ejecutar callbacks de E/S (la mayor parte del trabajo ocurre aquí)
├───────────────┤
│ check │ callbacks de setImmediate
├───────────────┤
│ close cbs │ ej. socket.on('close', ...)
└───────────────┘
│
└──────────── de vuelta a los timers, para siempre (mientras quede trabajo)
Ese es el esqueleto; Fases de libuv cubre lo que realmente sucede dentro de cada una, incluido el comportamiento de bloqueo de la fase de sondeo y cómo decide cuándo avanzar. Lo que importa a este nivel es la forma: es una secuencia fija, no una única cola de primero en entrar, primero en salir. La fase de una devolución de llamada determina aproximadamente cuándo puede ejecutarse, no el orden en que la registraste.
Las microtareas (Promesas resueltas, queueMicrotask, process.nextTick) se sitúan completamente fuera de ese ciclo de fases. Node vacía la cola de microtareas completamente después de cada devolución de llamada, no solo una vez por vuelta completa, por lo que las microtareas siempre tienen la oportunidad de ejecutarse antes de que el bucle pase de una fase (o una devolución de llamada) a la siguiente. Por eso un Promise.resolve().then() supera de forma fiable a un setTimeout(fn, 0) aunque ambos parezcan "ejecutarse pronto": uno es una macrotarea esperando su fase, el otro se intercala inmediatamente después de que la devolución de llamada actual termine. Microtareas vs. Macrotareas cubre las reglas de ordenación exactas, incluyendo dónde encaja process.nextTick.
La fase de sondeo merece una mención especial porque es donde el bucle pasa la mayor parte de su tiempo en una aplicación limitada por E/S: cuando no hay nada más que hacer, libuv puede dejar que el sondeo se bloquee, esperando en el sistema operativo nuevos eventos de E/S, en lugar de girar y consumir CPU. Así es como el bucle es eficiente en reposo: no está en espera activa, está dormido hasta que el sistema operativo lo despierte.
El bucle de eventos es un punto en un espectro de modelos de concurrencia, y saber dónde se sitúa aclara para qué es bueno y para qué no:
Modelo
Fortaleza
Debilidad
Mejor ajuste
Bucle de eventos de Node (cooperativo, un solo hilo JS)
E/S barata y de alta concurrencia; modelo mental simple, sin condiciones de carrera en JS
Una devolución de llamada larga detiene todo lo que comparte ese hilo
Servidores limitados por E/S, proxies, pasarelas en tiempo real
Hilo por solicitud del SO (preventivo)
El SO puede interrumpir una solicitud lenta; paralelismo verdadero
Costo de memoria/programación por hilo inactivo; condiciones de carrera que gestionar
Cargas de trabajo intensivas en CPU o de bloqueo por defecto
Hilos verdes / goroutines (ej. Go)
Unidades ligeras y preventivas programadas por el tiempo de ejecución
Modelo de memoria/GC diferente para razonar
Cargas de trabajo mixtas de CPU y E/S de alta concurrencia
Modelo de actor (ej. Erlang/BEAM)
Aislamiento verdadero por proceso; un actor fallido no detiene a otros
Mayor sobrecarga de paso de mensajes
Sistemas tolerantes a fallos, altamente concurrentes de estilo telecomunicaciones
La elección de Node, la programación cooperativa en un solo hilo, es la razón por la que la plataforma es inusualmente buena para la E/S de alta concurrencia e inusualmente mala para absorber silenciosamente un error ligado a la CPU. No hay una red de seguridad a nivel del sistema operativo; el bucle confía en que cada devolución de llamada devolverá el control rápidamente. Esta es también la razón por la que los autores de frameworks (Express, Fastify, NestJS) construyen el enrutamiento y el middleware en torno a la suposición de que los manejadores devuelven el control rápidamente: un middleware de bloqueo no solo ralentiza su propia solicitud, sino que retrasa toda la ronda del guardia.
En producción, esa naturaleza cooperativa es exactamente lo que estás vigilando: un aumento en el retraso o la utilización del bucle de eventos (consulta Detección de bloqueo del bucle de eventos) significa que alguna devolución de llamada, en algún lugar, no está cooperando. Mejores prácticas del bucle de eventos convierte este modelo en reglas operativas concretas: qué mantener fuera del hilo principal y cómo estructurar los manejadores para que el bucle permanezca libre.
"El bucle de eventos es una única cola, y las devoluciones de llamada se ejecutan en el orden en que se registraron." Es una secuencia fija de fases, cada una con su propia cola; el tipo de una devolución de llamada (temporizador, E/S, setImmediate) determina qué fase la maneja, no solo el orden de registro.
"Las Promesas y setTimeout se programan de la misma manera, solo con diferentes retrasos." Son categorías completamente diferentes: las Promesas son microtareas que se agotan entre cada devolución de llamada; las devoluciones de llamada de setTimeout son macrotareas vinculadas a una fase específica. No son comparables por "retraso".
"El bucle sondea constantemente en una espera activa, consumiendo CPU incluso cuando está inactivo." La fase de sondeo puede bloquearse y permitir que el sistema operativo lo despierte cuando la E/S esté lista; un proceso de Node inactivo no está haciendo girar un núcleo de CPU.
"Si uso async/await, el bucle de eventos no puede quedarse atascado en mi código."async cambia cómo se entrega un resultado, no si las partes síncronas de esa función pueden ocupar la pila de llamadas. Un bucle largo dentro de una función async bloquea exactamente tanto como lo haría fuera de una.
"El bucle de eventos garantiza que mi devolución de llamada se ejecute en un momento específico." Garantiza reglas de ordenación relativas (microtareas antes de la siguiente fase, fase de temporizadores antes de sondeo, etc.), nunca una marca de tiempo exacta; setTimeout(fn, 0) significa "no antes de", no "exactamente en".
¿Qué es el bucle de eventos de Node.js, en una frase?
Un ciclo repetitivo de fases fijas, ejecutado por libuv, que decide qué devolución de llamada pendiente obtiene el único hilo de JavaScript a continuación, y solo avanza una vez que la pila de llamadas de la devolución de llamada actual está vacía.
¿Por qué Node usa un bucle de eventos en lugar de un hilo por solicitud?
Porque la mayor parte del trabajo del servidor se dedica a esperar E/S, no a calcular; un solo hilo que nunca se bloquea en E/S puede atender muchas más conexiones concurrentes que un hilo por conexión, la mayoría de las cuales permanecerían inactivas esperando. Consulta Cómo funciona Node.js para conocer el razonamiento completo de V8/libuv.
¿Cómo se relaciona el bucle de eventos con la pila de llamadas?
El bucle solo puede elegir la siguiente devolución de llamada una vez que la pila de llamadas está completamente vacía; la regla de ejecución hasta la finalización de JavaScript significa que nada interrumpe una función en ejecución, por lo que la parte del "bucle" es realmente solo "lo que sucede entre una pila vacía y la siguiente".
¿Las microtareas forman parte del ciclo de fases?
No, las microtareas (Promesas, queueMicrotask, process.nextTick) se agotan completamente después de cada devolución de llamada, independientemente de la fase que se acaba de ejecutar. Tienen prioridad sobre el paso a la siguiente fase o la siguiente macrotarea.
¿Por qué un `setTimeout(fn, 0)` se ejecuta después de un `Promise.resolve().then()`?
La devolución de llamada de la Promesa es una microtarea y se agota inmediatamente después de que el código que se está ejecutando actualmente termina. La devolución de llamada del temporizador es una macrotarea que tiene que esperar a que el bucle alcance la fase de temporizadores en una pasada posterior; las microtareas siempre ganan esa carrera.
¿Qué es lo que realmente mantiene vivo un proceso de Node?
Cualquier temporizador pendiente, manejador abierto (como un servidor o socket en escucha) u operación de E/S en cola. Una vez que no queda ninguno de ellos, el bucle no tiene nada más que ciclar y el proceso sale de forma natural; .unref() en un manejador lo excluye de contar para "mantener el proceso vivo".
¿El bucle de eventos realiza una espera activa cuando no hay nada que hacer?
No. La fase de sondeo puede bloquearse, permitiendo que el sistema operativo despierte el proceso cuando la E/S esté realmente lista, en lugar de girar y consumir CPU mientras está inactivo.
¿Por qué una solicitud lenta afecta a solicitudes que no tienen nada que ver con ella?
Todas comparten la misma pila de llamadas. Una sección síncrona y ligada a la CPU de cualquier devolución de llamada ocupa completamente el único hilo de JS, por lo que el bucle no puede avanzar a ninguna otra devolución de llamada pendiente, incluidas las de solicitudes que no están relacionadas, hasta que regresa.
¿El bucle de eventos del navegador es el mismo que el de Node?
Comparten la misma idea central (ejecutar hasta la finalización, agotar microtareas y luego manejar la siguiente tarea), pero los detalles difieren. Los navegadores intercalan la renderización y otras tareas específicas de la interfaz de usuario en su bucle; el bucle de Node se basa en el modelo de fases de libuv (temporizadores, sondeo, verificación, etc.) sin ningún paso de renderización.
¿Se puede anticipar el bucle de eventos, de la misma manera que un sistema operativo anticipa los hilos?
No, es cooperativo. Nada obliga a una devolución de llamada de larga duración a ceder el control al bucle; tiene que regresar por sí misma (o entregar el trabajo a worker_threads/el pool de hilos de libuv) para que cualquier otra cosa se ejecute.
¿Cuál es la diferencia entre una "fase" y una "cola de microtareas"?
Una fase es una parada en el circuito fijo del bucle (temporizadores, sondeo, verificación, ...), cada una manejando una categoría de macrotarea. La cola de microtareas no es una fase en absoluto; se agota por completo después de cada devolución de llamada, independientemente de la fase que se acaba de ejecutar, por lo que las microtareas se ejecutan consistentemente antes que el trabajo de la siguiente fase.
¿Múltiples procesos de Node comparten un bucle de eventos?
No, cada proceso de Node tiene su propio bucle de eventos independiente y su propio hilo JS único. Herramientas como el módulo cluster o un administrador de procesos ejecutan múltiples procesos separados, cada uno con su propio bucle, para usar más de un núcleo de CPU.