Todo en Cómo funciona Node.js y El bucle de eventos de Node.js describe un único hilo JavaScript dentro de un único proceso del sistema operativo.
Ese modelo es rápido para E/S, pero no tiene respuesta para el trabajo ligado a la CPU, para ejecutar otro programa o para usar más de un núcleo de CPU.
Esta página trata sobre las tres formas en que Node te permite salir de ese único hilo —child_process, worker_threads y cluster— y el modelo mental compartido que hace que sus diferencias sean predecibles en lugar de arbitrarias.
Conceptos básicos de procesos explica el código de trabajo para cada uno de ellos; child_process, worker_threads y Módulo cluster profundizan en la sintaxis y los patrones de producción para cada uno individualmente.
Aquí, el objetivo es el mapa: qué cambia realmente —memoria, costo de inicio, aislamiento de fallas— a medida que pasas de permanecer en el proceso a generar uno completamente nuevo.
Las vías de escape de concurrencia de Node se sitúan en un espectro que va desde "compartir todo" (permanecer en el proceso) hasta "no compartir nada" (un proceso de SO separado), y cada opción intercambia aislamiento por costo de una manera específica y predecible.
Por qué es importante: Elegir la primitiva incorrecta —un nuevo proceso para una tarea que solo necesitaba un hilo, o un pool de hilos para una tarea que necesitaba aislamiento de fallas a nivel de proceso— o bien desperdicia recursos o permite que una falla derribe más de lo que debería.
Conceptos clave:Proceso del SO, hilo, aislado V8, clon estructurado, memoria compartida, canal IPC.
Cuándo usar este modelo: Para decidir si el trabajo ligado a la CPU pertenece a un hilo worker, si una tarea necesita un proceso hijo completo, si cluster es la forma correcta de usar múltiples núcleos, y para leer las páginas por primitiva que siguen con el marco correcto ya establecido.
Limitaciones / Compromisos: Ninguna de estas primitivas es gratuita: los procesos cuestan memoria y tiempo de inicio, los hilos cuestan serialización en cada mensaje, y ambos añaden una superficie operativa real (más cosas que pueden fallar, más cosas que monitorear).
Temas relacionados: el bucle de eventos, child_process, worker_threads, el módulo cluster, cierre elegante.
Un proceso del SO es la unidad de aislamiento del sistema operativo: su propio espacio de memoria, sus propios descriptores de archivo, su propio dominio de fallas.
Cuando un proceso falla, el sistema operativo garantiza que no puede corromper la memoria de otro proceso; esa garantía es exactamente lo que estás comprando cuando generas uno.
Un hilo, por el contrario, es una unidad de ejecución que se ejecuta dentro de un proceso y, en la mayoría de los lenguajes, comparte la memoria de ese proceso directamente con todos los demás hilos en él.
Los worker_threads de Node doblan ligeramente esa segunda definición: cada worker obtiene su propio aislado V8 —su propio heap de JavaScript, su propio recolector de basura, su propio bucle de eventos— aunque todos ellos viven dentro de un solo proceso del SO.
Es por eso que los workers se sienten más cercanos a los "procesos" en comportamiento (estado JS aislado, paso de mensajes en lugar de variables compartidas) mientras que siguen siendo más baratos de generar que un verdadero proceso del SO.
Compartir memoria entre workers es posible, pero solo deliberadamente, a través de un SharedArrayBuffer y Atomics; el valor predeterminado es copiar, no compartir.
Una analogía útil: permanecer en el proceso es un empleado manejando cada solicitud en un solo escritorio.
Generar un child_process es abrir una segunda oficina, completamente independiente, al otro lado de la ciudad; nada se comparte, y si se quema, tu oficina no se ve afectada, pero cada encargo allí cuesta un viaje.
Un worker de worker_threads es más como traer un segundo empleado al mismo edificio, en una habitación separada; pueden pasarse notas por debajo de la puerta (mensajes), y solo ven el escritorio del otro si explícitamente conectas una pizarra compartida (SharedArrayBuffer).
// Misma tarea, tres niveles de aislamiento diferentesnew Worker('./task.js', { workerData }); // proceso compartido, aislado V8 separadospawn('some-binary', ['--flag']); // proceso de SO separado, sin memoria compartidacluster.fork(); // proceso de SO separado, socket de escucha compartido
child_process genera un proceso del SO genuinamente separado; puede ejecutar cualquier ejecutable, no solo scripts de Node, y no comparte nada con el padre por defecto.
La comunicación ocurre a través de tuberías stdio, o a través de un canal IPC dedicado cuando usas fork() específicamente (que inicia un hijo de Node y configura los eventos process.send()/message por ti).
Esta es la opción más pesada en costo de inicio y memoria, y también es el aislamiento más fuerte: una falla en el proceso hijo no puede corromper la memoria del padre.
worker_threads crea un nuevo hilo dentro del mismo proceso, con su propio heap V8 aislado pero recursos compartidos a nivel de SO como descriptores de archivo.
El inicio es significativamente más barato que generar un proceso, y workerData clona una carga útil de inicio una vez; la comunicación continua a través de postMessage utiliza el algoritmo de clonación estructurada, que copia datos en lugar de compartirlos.
Una excepción no capturada dentro de un worker termina ese worker específicamente; el hilo principal y cualquier otro worker siguen ejecutándose, siempre que algo esté escuchando el evento 'error' del worker para observar lo que sucedió.
cluster no es una cuarta primitiva independiente; mecánicamente, es child_process.fork() bajo el capó, envuelto con lógica que permite que múltiples procesos de Node bifurcados compartan un socket de escucha.
El sistema operativo (o la propia lógica round-robin de Node, dependiendo de la plataforma) distribuye las conexiones entrantes entre esos procesos, que es cómo un tiempo de ejecución de un solo hilo usa más de un núcleo de CPU para un servidor HTTP.
Debido a que cada worker de clúster es un proceso separado completo, ninguno de ellos comparte el estado en memoria; una caché calentada en un worker es invisible para los demás, lo cual es una fuente común de confusión para cualquiera que asuma que cluster se comporta como un pool de hilos.
La consecuencia práctica de todo esto: worker_threads es para JavaScript ligado a la CPU que necesita salir del hilo principal sin la sobrecarga de generar procesos; child_process es para ejecutar otros programas o para un aislamiento lo suficientemente fuerte como para que una falla realmente no pueda afectar al resto; cluster es para usar múltiples núcleos para servir un tipo de trabajo orientado a la red, a costa de memoria duplicada y sin caché compartida.
El aislamiento de fallas es el eje que la mayoría de los equipos subestiman hasta que un incidente fuerza la pregunta.
Un fallo de un hilo worker está contenido en ese worker, pero aún comparte el proceso del SO con todo lo demás; un fallo de código nativo lo suficientemente grave dentro de un worker (un segfault en un complemento nativo, por ejemplo) aún puede derribar todo el proceso, incluidos los workers.
Un fallo de un proceso hijo está contenido incluso de eso: el límite de proceso del sistema operativo es una garantía mucho más fuerte que el límite de aislado de V8.
La escalabilidad con cluster es un patrón legítimo en un único host multinúcleo, pero compite con una respuesta diferente, cada vez más común, al mismo problema: escalar réplicas horizontalmente bajo un orquestador como Kubernetes.
Ambos enfoques resuelven "usar más de un núcleo", pero lo resuelven en diferentes capas —cluster dentro de una implementación de Node, orquestación a través de muchas implementaciones de un solo hilo— y ejecutar ambos a la vez generalmente solo añade complejidad sin añadir capacidad.
Las consideraciones de seguridad y observabilidad difieren drásticamente según la primitiva.
child_process con shell: true y entrada no saneada es un vector directo de inyección de comandos; pasar un array de argumentos a spawn en lugar de una cadena de shell evita por completo esa clase de error.
Cada primitiva aquí también multiplica tu superficie de monitoreo: un PID por proceso hijo, un threadId por worker, y el seguimiento de CPU/memoria por worker son todos necesarios una vez que ya no estás mirando un solo bucle de eventos.
El ecosistema ha convergido en los pools de workers como la unidad práctica de adopción para el trabajo ligado a la CPU, en lugar de generar workers ad hoc por solicitud; bibliotecas como piscina gestionan un pool fijo, la cola y el ciclo de vida para que el código de la aplicación no reimplemente la programación.
Enfoque
Fortaleza
Debilidad
Mejor ajuste
Permanecer en el proceso (async/await)
Cero sobrecarga, modelo mental más simple
Ninguna ayuda para el trabajo ligado a la CPU
Manejadores ligados a E/S, el valor predeterminado abrumador
worker_threads
Barato de generar, fallas aisladas, memoria compartida opcional
Costo de serialización por mensaje a menos que se use SharedArrayBuffer
JS ligado a la CPU: hashing, procesamiento de imágenes, grandes transformaciones de datos
child_process
Aislamiento más fuerte, puede ejecutar binarios que no son de Node
Mayor costo de generación, ninguna memoria compartida
Ejecutar herramientas externas, aislar trabajo genuinamente no confiable o frágil
cluster
Utiliza múltiples núcleos para un único listener HTTP
Memoria duplicada por worker, sin caché compartida en el proceso
Un único host multinúcleo que sirve un servicio ligado a HTTP
"Node es de un solo hilo, por lo que no puede usar múltiples núcleos de CPU." El hilo principal de JavaScript es de un solo hilo, pero worker_threads, child_process y cluster existen específicamente para usar más de un núcleo cuando el trabajo lo justifica.
"worker_threads y child_process son lo mismo con nombres diferentes." Difieren en el nivel de aislamiento y el costo: los workers comparten un proceso y por defecto copian mensajes; los procesos hijos están completamente separados, sin memoria compartida en absoluto.
"cluster proporciona a los workers memoria compartida." Los workers de clúster son procesos del SO separados; nada se comparte automáticamente, y cualquier estado que un worker necesite en común con sus hermanos debe residir en algún lugar externo, como Redis o una base de datos.
"Generar un worker por solicitud es la forma de paralelizar el trabajo de la CPU." Eso recrea el problema exacto de sobrecarga que los workers deben evitar: el código de producción utiliza un pool de tamaño fijo, reutilizado en varias solicitudes.
"Un worker o proceso hijo que falla siempre derriba toda la aplicación." El aislamiento es el objetivo: una excepción no capturada de un worker termina ese worker, y el fallo de un proceso hijo permanece dentro del límite del proceso del SO, siempre que algo esté atento al evento de fallo.
¿Cuál es la forma más sencilla de decidir entre permanecer en el proceso, un hilo worker y un proceso hijo?
Primero pregunta qué tipo de trabajo es: la E/S pura permanece en el proceso, el JavaScript ligado a la CPU va a un pool de worker_threads, y ejecutar otro programa (o necesitar un aislamiento fuerte a nivel de proceso) va a child_process.
¿Es `cluster` un mecanismo subyacente diferente de `child_process`?
No, cluster se basa en child_process.fork(), con lógica adicional para compartir un socket de escucha entre los procesos bifurcados para que un servidor HTTP pueda usar múltiples núcleos de CPU.
¿Los hilos worker comparten memoria con el hilo principal por defecto?
No, postMessage y workerData copian datos utilizando el algoritmo de clonación estructurada; compartir memoria real requiere el uso explícito de un SharedArrayBuffer con Atomics.
¿Por qué generar un worker de `worker_threads` es más barato que generar un `child_process`?
Un worker permanece dentro del mismo proceso del SO y solo crea un nuevo aislado V8 y un hilo, mientras que un proceso hijo le pide al sistema operativo un proceso completamente nuevo con su propio espacio de memoria y descriptores de archivo, una operación mucho más pesada.
¿Puede un fallo en un worker de clúster derribar todo el servidor?
No por defecto: cada worker de clúster es un proceso del SO separado, por lo que el fallo de uno no corrompe a los demás, aunque el proceso primario debe estar escrito para detectar la salida y bifurcar un reemplazo.
¿Qué sucede si un hilo worker lanza una excepción no capturada?
Ese worker específico termina; el hilo principal y cualquier worker hermano siguen ejecutándose, siempre que el código que creó el worker esté escuchando su evento 'error'.
¿Cuándo es `cluster` la herramienta incorrecta aunque "use más núcleos"?
Cuando ya estás ejecutando múltiples réplicas bajo un orquestador como Kubernetes; combinar la escalabilidad horizontal a nivel de pod con la bifurcación cluster en el proceso generalmente añade complejidad operativa sin añadir capacidad real.
¿Por qué `spawn('cmd', args)` importa más que `exec('cmd ' + args)` para la seguridad?
spawn con un array de argumentos pasa los argumentos directamente al SO sin pasar por un shell, por lo que la entrada controlada por el usuario en un argumento no puede interpretarse como sintaxis de shell; exec (o spawn con shell: true) abre esa puerta.
¿Necesito una biblioteca de pool de workers, o puedo gestionar los workers yo mismo?
Puedes gestionar un pequeño pool fijo tú mismo, pero bibliotecas como piscina manejan la cola, la contrapresión y el ciclo de vida correctamente; reimplementar eso desde cero es fácil de hacer sutilmente mal bajo carga.
¿Ayuda `worker_threads` si mi cuello de botella es en realidad la E/S, no la CPU?
No, el trabajo ligado a la E/S (llamadas de red, la mayoría de las consultas a bases de datos) ya cede el bucle de eventos a través de await y no obtiene ningún beneficio de un hilo separado; los workers solo ayudan cuando el trabajo es genuinamente JavaScript ligado a la CPU.
¿Cuál es la diferencia entre `child_process.exec` y `child_process.fork`?
exec ejecuta un comando de shell arbitrario y almacena su salida en un búfer; fork inicia específicamente un nuevo proceso de Node.js que ejecuta un script dado y configura un canal IPC para el paso de mensajes estructurados entre el padre y el hijo.
¿Hay alguna forma de compartir una caché real en memoria entre workers de clúster?
No directamente; dado que los workers de clúster son procesos separados, una caché compartida debe residir fuera de cualquier worker individual, típicamente en Redis u otro almacén externo que todos los workers puedan leer y escribir.