Node.js es JavaScript ejecutándose fuera de un navegador, pero la parte interesante no es el lenguaje, sino el tiempo de ejecución que lo sustenta. Node combina el motor V8 de Google con una librería C llamada libuv para darle a JavaScript, un lenguaje sin concepto nativo de hilos o E/S asíncrona, una forma de manejar miles de conexiones concurrentes usando un solo hilo de ejecución.
Ese único hecho —un hilo de JS, E/S no bloqueante en todas partes— explica casi todas las preguntas de "por qué mi servidor está haciendo eso" que un desarrollador de Node eventualmente se hace: por qué una solicitud lenta detiene otras no relacionadas, por qué worker_threads existen, y por qué la escalabilidad horizontal se ve diferente aquí que en un servidor de hilo por solicitud. Comprender este modelo es lo más útil que puedes aprender antes de escribir código Node en producción.
Node ejecuta tu JavaScript en un solo hilo y delega la E/S al sistema operativo y a un pequeño pool de hilos, coordinado por un bucle de eventos que reanuda tu código cuando el trabajo se completa.
Por qué es importante: Explica la mayor fortaleza de Node (gran concurrencia de E/S en hardware modesto) y su punto más débil (una función síncrona ligada a la CPU puede congelar todas las demás solicitudes).
Conceptos clave:V8, libuv, el bucle de eventos, el pool de hilos, E/S no bloqueante, microtareas.
Cuándo usar este modelo: Para razonar sobre picos de latencia, elegir entre worker_threads/child_process/cluster, explicar a un compañero por qué un bucle for derribó la API, y dimensionar cargas de trabajo ligadas al pool de hilos (criptografía, compresión, algunas llamadas fs/DNS).
Limitaciones / Compromisos: Excelente para cargas de trabajo ligadas a E/S y de alta concurrencia; una opción predeterminada deficiente para trabajos por lotes intensivos en CPU a menos que optes deliberadamente por el paralelismo.
Temas relacionados: Fases del bucle de eventos, internos de libuv, hilos de trabajo, Node vs. Bun vs. Deno.
Antes de Node (2009), JavaScript del lado del servidor no era realmente una cosa, y la mayoría de las plataformas de servidor manejaban la concurrencia creando un hilo (o proceso) por conexión. Ese modelo es simple de razonar pero costoso: los hilos inactivos aún consumen memoria y son programados por el sistema operativo, por lo que la concurrencia está limitada por la cantidad de hilos que una máquina puede manejar.
El creador de Node, Ryan Dahl, invirtió eso: en lugar de un hilo por conexión, Node usa un hilo de JavaScript para todo, y nunca deja que ese hilo esté inactivo esperando E/S. Cuando tu código pide leer un archivo, consultar una base de datos o hacer una solicitud HTTP, Node entrega la parte de espera al sistema operativo (o a un hilo auxiliar), devuelve inmediatamente el control al hilo de JS y llama a tu código solo una vez que el resultado está listo.
Un modelo mental útil: imagina un único controlador de tráfico aéreo que nunca pilota un avión personalmente. Cada despegue y aterrizaje (una operación de E/S, una lectura de archivo, una escritura de socket) es manejado por los pilotos y el personal de tierra (el sistema operativo y libuv) mientras el controlador sigue dirigiendo el nuevo tráfico. En el momento en que un avión aterriza, el controlador recibe una llamada de radio y reacciona a ella, pero si el controlador alguna vez tiene que apartarse y resolver personalmente un problema matemático (trabajo síncrono de CPU), todos los demás aviones esperan hasta que eso se complete. Node es rápido dirigiendo el tráfico y completamente bloqueado mientras hace aritmética.
Dos piezas hacen esto posible:
V8: el mismo motor JavaScript que usa Chrome. Compila tu JS a código máquina, lo ejecuta y gestiona la memoria/recolección de basura. V8 no sabe nada sobre servidores, archivos o sockets.
libuv: una librería C que le da a Node E/S asíncrona multiplataforma, temporizadores y un pool de hilos. Es la capa que realmente se comunica con las primitivas asíncronas del sistema operativo (epoll en Linux, kqueue en macOS/BSD, IOCP en Windows) e impulsa el bucle de eventos que une todo.
El bucle de eventos no es una cola a la que empujas callbacks directamente, es un conjunto fijo de fases por las que libuv cicla, cada una responsable de un tipo diferente de trabajo pendiente: temporizadores (setTimeout/setInterval), callbacks de E/S pendientes, sondeo (recuperación de nuevos eventos de E/S), verificación (setImmediate) y callbacks de cierre. Entre casi cada paso, Node vacía la cola de microtareas (Promesas resueltas y callbacks de queueMicrotask) antes de continuar, por lo que Promise.resolve().then() se ejecuta de forma fiable antes de un setTimeout(fn, 0), aunque ambos parezcan "ejecutarse pronto".
La mayoría de las operaciones de E/S en Node se mapean directamente a una API asíncrona a nivel del sistema operativo, por lo que libuv puede entregarlas sin hilos adicionales involucrados: el kernel notifica a libuv, libuv notifica al bucle de eventos, el bucle de eventos llama a tu JS. Pero un puñado de operaciones no tienen un equivalente asíncrono en el sistema operativo (algunas llamadas fs, crypto.pbkdf2, búsquedas DNS y compresión zlib), por lo que libuv las ejecuta en un pequeño pool de hilos, cuatro hilos por defecto, dimensionado a través de UV_THREADPOOL_SIZE. Esta distinción importa operativamente: la saturación del hilo principal se manifiesta como la CPU al 100%; la saturación del pool de hilos se manifiesta como solicitudes en cola silenciosamente mientras la CPU parece estar bien.
Puedes observar este modelo directamente en lugar de solo razonar sobre él: perf_hooks expone la propia salud del bucle de eventos como una métrica:
Un p99 creciente aquí significa que algo (un bucle síncrono, un JSON.parse enorme, un retraso en el pool de hilos) está impidiendo que el único hilo de JS vuelva al bucle de eventos rápidamente, lo que retrasa todos los demás callbacks pendientes, no solo el lento.
Debido a que la concurrencia proviene de no bloquear, el modo de fallo del modelo es específico y predecible: cualquier operación síncrona y ligada a la CPU bloquea todo el proceso, no solo la solicitud que la activó. Un solo bucle for que hashea datos, un JSON.parse síncrono grande o un complemento nativo que llama a JS de forma síncrona detendrá todas las demás solicitudes en curso en ese proceso, independientemente de cuántos clientes estén conectados o cuán "asíncrono" parezca el resto del código.
Node deliberadamente no resuelve esto por ti, te da herramientas explícitas para optar por el paralelismo cuando lo necesitas:
Enfoque
Fortaleza
Debilidad
Mejor ajuste
worker_threads
Ejecuta JS ligado a la CPU fuera del hilo principal, en el proceso
Sobrecarga de paso de mensajes; no es un aislamiento completo
Hashing, procesamiento de imágenes/datos, análisis de grandes cargas útiles
child_process
Aislamiento completo a nivel del sistema operativo; puede ejecutar herramientas que no son de Node
Mayor sobrecarga; no hay memoria compartida por defecto
Ejecución de CLIs, ejecución de un tiempo de ejecución diferente
cluster module
Utiliza múltiples núcleos para HTTP en una máquina
Cada worker tiene su propia memoria/bucle de eventos
Rendimiento multinúcleo sin un orquestador
Réplicas horizontales / cola externa
Escala más allá de una máquina; desacopla el trabajo reintentable
Salto de red; necesita infraestructura (LB, cola)
Despliegues de Kubernetes/nube, trabajos largos o reintentables
Esta es también la razón por la que el ajuste de Node tiene forma de carga de trabajo, no es universal: sobresale en trabajos ligados a E/S y de alta concurrencia (APIs, proxies, streaming, pasarelas en tiempo real) donde la mayor parte del tiempo se dedica a esperar una red o disco, no a computar. Es una opción predeterminada más débil para trabajos por lotes intensivos en CPU (codificación de video, grandes simulaciones numéricas) a menos que ese trabajo se mueva explícitamente fuera del hilo principal. Los tiempos de ejecución de JS más nuevos como Bun y Deno mantienen el mismo modelo fundamental de bucle de eventos (todavía incrustan V8 o un motor similar a V8); difieren en herramientas, tiempo de inicio y APIs integradas, no en esta forma de concurrencia central; consulta Node.js vs. Bun vs. Deno para esa comparación.
El modelo se ha mantenido notablemente estable a lo largo de la historia de Node: Node 24 incluye un V8 más nuevo, un fetch integrado estable y un node:test mejorado, pero la arquitectura V8-más-libuv-más-hilo-único subyacente no ha cambiado desde las primeras versiones de Node. En producción, la salud del bucle de eventos (métricas de retraso/utilización como el fragmento anterior) es una de las cosas de mayor señal para poner en un panel de control, porque se degrada antes de que las tasas de error lo hagan típicamente.
"Node es de un solo hilo, punto." Solo tu JavaScript está confinado a un hilo. El pool de hilos de libuv, el manejo de E/S del sistema operativo y el recolector de basura de V8 pueden usar hilos adicionales detrás de escena; "un solo hilo" describe dónde se ejecutan tus callbacks, no todo el proceso.
"async/await crea un nuevo hilo para ejecutar el trabajo esperado." No lo hace. await simplemente pausa esa función y devuelve el control al bucle de eventos; cuando la operación subyacente se completa, la continuación se reanuda como una microtarea en el mismo hilo JS.
"setImmediate se ejecuta inmediatamente, antes de otro trabajo pendiente." Se ejecuta en la fase de verificación, después de los callbacks de E/S de la fase de sondeo; se ordena en relación con las fases del bucle de eventos, no es un sinónimo de "tan pronto como sea posible".
"Más núcleos de CPU significan automáticamente más rendimiento de Node." Un solo proceso de Node usa un núcleo para JavaScript, sin importar cuántos núcleos tenga la máquina. Usar el resto requiere una elección explícita: cluster, múltiples procesos o worker_threads.
"Si mi código usa async/await en todas partes, no puede bloquear el bucle de eventos." Cualquier instrucción síncrona dentro de una función asíncrona aún se ejecuta de forma síncrona. async solo cambia cómo se entrega el resultado de la función, no si su código puede detener el hilo mientras se ejecuta.
La ejecución de JavaScript dentro de un único proceso de Node es de un solo hilo; tus callbacks se ejecutan todos en un solo hilo. libuv utiliza hilos adicionales internamente (su pool de hilos, además del manejo asíncrono a nivel del sistema operativo), pero ninguno de ese trabajo visible para JavaScript se ejecuta en paralelo con tu código.
¿Qué hace realmente libuv?
libuv es la librería C que le da a Node su bucle de eventos, E/S asíncrona multiplataforma (envolviendo epoll/kqueue/IOCP), temporizadores y un pool de hilos para el puñado de operaciones sin equivalente asíncrono en el sistema operativo. V8 ejecuta tu JavaScript; libuv es lo que hace que ese JavaScript pueda realizar E/S no bloqueantes.
¿Por qué una solicitud lenta ralentiza todas las demás solicitudes?
Cada manejador de solicitudes en un proceso de Node comparte el mismo hilo único de JavaScript. Una sección de código síncrona y ligada a la CPU (un bucle grande, un JSON.parse grande) ocupa ese hilo por completo, por lo que el callback de ningún otro manejador puede ejecutarse hasta que termine, incluso si esas otras solicitudes estaban listas para completarse instantáneamente.
¿Cuántos hilos usa Node realmente por defecto?
Un hilo ejecuta tu JavaScript. libuv además ejecuta un pool de hilos (cuatro hilos por defecto, configurable a través de UV_THREADPOOL_SIZE) para operaciones como algunas llamadas fs, crypto.pbkdf2, búsquedas DNS y zlib, además de los hilos que el recolector de basura de V8 y el sistema operativo usan internamente.
¿`await` inicia un nuevo hilo?
No. await pausa la función actual y devuelve el control al bucle de eventos. Cuando la operación esperada finaliza, su continuación se programa como una microtarea que se ejecuta en el mismo hilo JS original; no se crea ningún hilo nuevo para el await en sí.
¿Cuál es la diferencia real entre V8 y Node?
V8 es solo el motor JavaScript, el mismo que usa Chrome, y no sabe nada sobre archivos, sockets o servidores. Node es V8 más libuv (bucle de eventos, E/S asíncrona, pool de hilos) más módulos integrados (fs, http, crypto, ...), el cargador de módulos y npm, las piezas que convierten "un motor JS" en "un tiempo de ejecución de servidor".
¿Por qué un bucle ligado a la CPU puede derribar una API completa, incluso bajo balanceo de carga?
El balanceo de carga distribuye las solicitudes entre procesos (o máquinas), pero dentro de un solo proceso, cada solicitud aún comparte un hilo JS. Un bucle ligado a la CPU en ese hilo bloquea todas las solicitudes actualmente enrutadas a ese proceso específico; la solución es descargar el trabajo (worker_threads), no solo agregar más instancias con balanceo de carga, a menos que también enrutes alrededor del proceso afectado.
¿Cuándo debo usar `worker_threads` versus `cluster` versus `child_process`?
worker_threads - JavaScript ligado a la CPU que necesita permanecer en el mismo proceso y puede compartir memoria a través de SharedArrayBuffer.
cluster - usar múltiples núcleos para tráfico HTTP en una sola máquina, con cada worker como su propio proceso independiente.
child_process - ejecutar un programa separado o un tiempo de ejecución completamente diferente, donde el aislamiento completo a nivel del sistema operativo vale la pena la sobrecarga.
¿El modelo de hilos de Node difiere significativamente de Bun o Deno?
No a nivel central; ambos aún ejecutan JavaScript en un solo hilo por proceso con un bucle de eventos de E/S no bloqueante. Las diferencias son principalmente herramientas, rendimiento de inicio y APIs integradas, no la forma de concurrencia subyacente.
¿Cuál es la diferencia práctica entre la saturación del pool de hilos y el bloqueo del hilo principal?
El bloqueo del hilo principal (un bucle síncrono) se manifiesta como la CPU al 100% con todo detenido. La saturación del pool de hilos (demasiadas llamadas pbkdf2/fs/zlib concurrentes) se manifiesta como solicitudes en cola silenciosamente detrás del tamaño fijo del pool, a menudo con la CPU con un aspecto normal; los síntomas apuntan a diferentes soluciones.
¿Cómo observo la salud del bucle de eventos en producción?
import { monitorEventLoopDelay } from 'node:perf_hooks';const h = monitorEventLoopDelay({ resolution: 20 });h.enable();
Muestra h.percentile(99) en un intervalo y envíalo a tu pipeline de métricas; un retraso creciente del bucle de eventos p99 es una de las primeras señales de que algo síncrono está deteniendo el proceso, a menudo antes de que las tasas de error se muevan.
¿Ha cambiado este modelo en las versiones recientes de Node?
No, Node 24 incluye un V8 más nuevo, un fetch integrado estable y un node:test mejorado, pero la arquitectura V8-más-libuv-más-hilo-JS-único es la misma que Node ha utilizado desde sus primeras versiones.
¿Por qué alguien elegiría Node en lugar de un tiempo de ejecución de servidor con hilos?
La E/S no bloqueante es una opción predeterminada sólida para cargas de trabajo de alta concurrencia y ligadas a E/S (APIs, proxies inversos, streaming, pasarelas en tiempo real) porque las conexiones inactivas no cuestan casi nada mientras esperan. Los tiempos de ejecución que por defecto usan un hilo (o proceso) por solicitud, en cambio, pagan una sobrecarga de memoria y programación por cada conexión inactiva, lo que se manifiesta como un techo de concurrencia práctico más bajo en el mismo hardware.