node:cluster fork-a múltiplos processos Node compartilhando a mesma porta HTTP. O processo primary gerencia o ciclo de vida; os workers atendem requisições. No Linux, o próprio Node distribui conexões em round-robin; no Windows, o kernel balanceia via IOCP. Útil em deploys single-VM onde não existe orquestrador — mas em prod com K8s, ECS ou similar, o orquestrador já faz isso por você, e cluster vira camada redundante.
Diagrama
flowchart TD
A(["`**primary**
process.pid`"]):::primary
B(["`**Worker 1**
port :3000`"]):::worker
C(["`**Worker 2**
port :3000`"]):::worker
D(["`**Worker 3**
port :3000`"]):::worker
E(["`**Worker 4**
port :3000`"]):::worker
F(["`**cliente HTTP**`"]):::client
A -- cluster.fork --> B
A -- cluster.fork --> C
A -- cluster.fork --> D
A -- cluster.fork --> E
F -- "TCP connection" --> A
A -- "round-robin (SCHED_RR)" --> B
A -- "round-robin (SCHED_RR)" --> C
A -- "round-robin (SCHED_RR)" --> D
A -- "round-robin (SCHED_RR)" --> E
classDef primary fill:#4A90D9,color:#fff,stroke:#2c6fad
classDef worker fill:#F5A623,color:#000,stroke:#c47d0e
classDef client fill:#eee,color:#333,stroke:#999
O que é
O módulo node:cluster permite que um único processo Node.js crie múltiplos processos filhos (workers) que compartilham a mesma porta TCP. O processo original, chamado primary, não atende requisições diretamente: ele fork-a os workers, monitora seus ciclos de vida e pode se comunicar com eles via IPC. Os workers rodam o mesmo arquivo de entrada e são distinguidos pela variável de ambiente NODE_UNIQUE_ID.
Processo Primary
│
├── cluster.fork() ──► Worker 1 ┐
├── cluster.fork() ──► Worker 2 ├── todos escutam porta 3000
├── cluster.fork() ──► Worker 3 │
└── cluster.fork() ──► Worker 4 ┘
│
(conexões distribuídas pelo Node/kernel)
Internamente, cluster.fork() usa child_process.fork() — portanto cada worker é um processo Unix separado, com heap própria e event loop próprio. Não há memória compartilhada entre workers (diferente de Worker Threads).
Por que importa
O event loop do Node é single-threaded. Numa máquina com 8 cores, um processo Node padrão usa ~12% da CPU em cenários CPU-bound e deixa os outros 7 cores ociosos. O cluster resolve esse problema no nível de processo.
Historicamente era a solução canônica para aproveitar todos os cores em deploy single-VM. Em 2026, esse papel foi em grande parte absorvido por orquestradores de container — mas o módulo ainda tem lugar:
Single-VM sem container: VPS simples, tooling interno, pequeno SaaS
Scripts que paralelizam HTTP: load tester, proxy local, mock server com múltiplos workers
Ambientes onde PM2 não está disponível e não há orquestrador
Entender a base: PM2 em modo cluster usa exatamente essa API por baixo
Quando NÃO usar
Se você tem K8s, ECS, Fly.io ou qualquer runtime que gerencia réplicas de container, não adicione cluster. Você estaria rodando, por exemplo, 4 réplicas de pod × 4 workers = 16 processos sem ganho adicional, com overhead de memória e complexidade operacional.
Como funciona: port sharing
O mecanismo que permite múltiplos processos escutarem a mesma porta depende do SO:
Linux (padrão — SCHED_RR):
O Node adota uma estratégia própria de round-robin sobre o file descriptor compartilhado. O primary aceita a conexão e a distribui para um worker disponível. Isso evita o problema clássico de SO_REUSEPORT no Linux onde alguns workers ficam sobrecarregados enquanto outros ficam ociosos — a distribuição desigual chegava a 70%+ das conexões indo para apenas 2 de 8 workers.
Windows:
O kernel distribui via IOCP (I/O Completion Ports), que é o mecanismo nativo do Windows para I/O assíncrono de alta performance.
Você pode alterar a política explicitamente:
import cluster from 'node:cluster';// Força round-robin (default no Linux)cluster.schedulingPolicy = cluster.SCHED_RR;// Delega ao SO (padrão no Windows, problemático no Linux)cluster.schedulingPolicy = cluster.SCHED_NONE;
import cluster from 'node:cluster';import { availableParallelism } from 'node:os';import http from 'node:http';if (cluster.isPrimary) { const numCPUs = availableParallelism(); // preferir sobre os.cpus().length console.log(`Primary ${process.pid} iniciando ${numCPUs} workers`); for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // Restart automático: sem isso, worker morto = capacidade reduzida silenciosamente cluster.on('exit', (worker, code, signal) => { console.log(`Worker ${worker.process.pid} encerrou (code=${code}, signal=${signal}). Reiniciando...`); cluster.fork(); });} else { // Cada worker roda este bloco de forma independente http.createServer((req, res) => { res.end(`Atendido pelo worker ${process.pid}\n`); }).listen(3000); console.log(`Worker ${process.pid} escutando na porta 3000`);}
availableParallelism() vs os.cpus().length
availableParallelism() respeita CPU affinity e cgroups — em containers com CPU limit configurado, devolve o número correto de cores disponíveis, não o total do host. Use sempre esta forma.
2. Separando primary e worker em arquivos distintos
Para codebases maiores, misturar lógica de primary e worker no mesmo arquivo fica confuso. O cluster permite separar:
// primary.jsimport cluster from 'node:cluster';import { availableParallelism } from 'node:os';cluster.setupPrimary({ exec: new URL('./worker.js', import.meta.url).pathname,});for (let i = 0; i < availableParallelism(); i++) { cluster.fork();}cluster.on('exit', (worker) => { console.warn(`Worker ${worker.id} morreu. Reiniciando.`); cluster.fork();});
// worker.jsimport http from 'node:http';http.createServer((req, res) => { res.end(`Worker ${process.pid}\n`);}).listen(3000);
3. IPC: primary e workers trocando mensagens
Workers e primary se comunicam via canal IPC (pipe Unix). Útil para agregar métricas sem banco externo:
O canal IPC passa mensagens serializadas (JSON por padrão). Para volumes altos ou objetos grandes, prefira Redis, banco ou SharedArrayBuffer com Worker Threads.
4. Graceful shutdown
// No primary: ao receber SIGTERM, desliga workers ordenadamenteprocess.on('SIGTERM', () => { console.log('SIGTERM recebido. Iniciando graceful shutdown...'); // Para de aceitar novas conexões em todos os workers cluster.disconnect(() => { console.log('Todos os workers desconectados. Encerrando primary.'); process.exit(0); }); // Força encerramento se demorar mais de 10s setTimeout(() => { console.error('Timeout no shutdown. Forçando saída.'); process.exit(1); }, 10_000);});
// No worker: drain de conexões existentesprocess.on('message', (msg) => { if (msg === 'shutdown') { server.close(() => { process.exit(0); }); }});
O fluxo completo:
SIGTERM → primary
│
├── cluster.disconnect()
│ │
│ ├── worker.disconnect() ──► worker para de aceitar novas conexões
│ │ worker drena conexões existentes
│ │ worker fecha IPC channel
│ │ worker.exitedAfterDisconnect = true
│ └── callback quando todos terminam
│
└── primary encerra
worker.exitedAfterDisconnect === true distingue saída graceful de crash — use isso no handler 'exit' para não reiniciar workers que saíram intencionalmente:
Com round-robin, requisições do mesmo cliente podem ir para workers diferentes a cada request. Para HTTP stateless isso é irrelevante — mas para WebSocket e SSE (Server-Sent Events) é fatal: a conexão persistente vai para o Worker 1, mas o próximo handshake pode ir para o Worker 2, que não tem contexto da sessão.
Solução 1: @socket.io/sticky (cluster interno)
import cluster from 'node:cluster';import { availableParallelism } from 'node:os';import { createServer } from 'node:http';import { setupPrimary, setupWorker } from '@socket.io/sticky';import { setupMaster } from '@socket.io/cluster-adapter';import { Server } from 'socket.io';if (cluster.isPrimary) { const httpServer = createServer(); setupMaster(httpServer, { loadBalancingMethod: 'least-connection' }); setupPrimary(); // roteamento sticky via primary httpServer.listen(3000); for (let i = 0; i < availableParallelism(); i++) { cluster.fork(); }} else { const httpServer = createServer(); const io = new Server(httpServer); setupWorker(io); httpServer.listen(0); // porta aleatória; primary roteia}
Solução 2: nginx com ip_hash (preferível em prod)
upstream nodejs_cluster { ip_hash; # garante que o mesmo IP sempre vai para o mesmo upstream server 127.0.0.1:3001; server 127.0.0.1:3002; server 127.0.0.1:3003; server 127.0.0.1:3004;}server { listen 80; location / { proxy_pass http://nodejs_cluster; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }}
Neste modelo, cada worker escuta em porta diferente (usando cluster.worker.id) e o nginx faz o roteamento sticky. O cluster ainda gerencia o ciclo de vida dos workers, mas o balanceamento fica com o nginx.
Na prática
Em 2026, o uso direto de node:cluster em código de produção novo é raro. Os cenários onde ainda faz sentido:
Cenário
Alternativa preferível
Cluster ainda ok?
Single VPS sem container runtime
PM2 em modo cluster
Sim
Lambda / Cloud Functions
Escalonamento horizontal da plataforma
Não
K8s com múltiplas réplicas
Aumentar réplicas do pod
Não
Dev local para simular concorrência
Cluster ou PM2
Sim
Script que precisa paralelizar HTTP
Cluster ou Worker Threads
Sim
Internal tooling em VM dedicada
PM2 ou cluster direto
Sim
Caminho recomendado em 2026
Para a maioria dos casos novos: 1 processo Node por container, deixe o orquestrador escalar horizontalmente. Para single-VM sem orquestrador: PM2 em modo cluster é mais ergonômico que cluster manual (health checks, logs, métricas embutidos). Cluster direto quando você precisa de controle fino ou não pode instalar PM2.
Veja [[10 - Cluster vs PM2 vs Kubernetes - quem orquestra]] para a análise comparativa completa.
Casos práticos
Caso 1 — API REST em VPS com 4 cores
Você tem um droplet de $10/mês na DigitalOcean com 4 vCPUs. Sem cluster, só 1 core fica saturado em pico. Com cluster, os 4 ficam disponíveis.
// server.js — cluster com 4 workers e watchdogimport cluster from 'node:cluster';import { availableParallelism } from 'node:os';import http from 'node:http';if (cluster.isPrimary) { const cpus = availableParallelism(); // 4 em nosso droplet for (let i = 0; i < cpus; i++) cluster.fork(); cluster.on('exit', (worker, code, signal) => { if (!worker.exitedAfterDisconnect) { console.warn(`Worker ${worker.id} morreu (code=${code}). Reiniciando...`); cluster.fork(); } }); // Graceful shutdown: ao receber SIGTERM, desliga workers ordenadamente process.on('SIGTERM', () => { cluster.disconnect(() => process.exit(0)); setTimeout(() => process.exit(1), 10_000); });} else { http.createServer((req, res) => { res.end(`pid=${process.pid}\n`); }).listen(3000);}
Resultado: 4 workers escutam na mesma porta :3000. O primary distribui conexões TCP em round-robin. Um worker crashando é substituído automaticamente sem downtime.
Caso 2 — Aggregando métricas de workers via IPC
Você quer um endpoint /metrics que some os request counts de todos os workers — sem banco externo, via IPC.
// primary — agrega contadores de todos os workersimport cluster from 'node:cluster';import { availableParallelism } from 'node:os';import http from 'node:http';const workerCounts = {};if (cluster.isPrimary) { for (let i = 0; i < availableParallelism(); i++) cluster.fork(); cluster.on('message', (worker, msg) => { if (msg.type === 'count') workerCounts[worker.id] = msg.value; }); // Endpoint de métricas no primary (porta diferente) http.createServer((req, res) => { if (req.url === '/metrics') { const total = Object.values(workerCounts).reduce((a, b) => a + b, 0); res.end(JSON.stringify({ total, perWorker: workerCounts })); } }).listen(9090);} else { let count = 0; const server = http.createServer((req, res) => { count++; process.send({ type: 'count', value: count }); res.end('ok'); }); server.listen(3000);}
Cuidado: IPC tem overhead de serialização JSON. Para métricas de alto volume, prefira Prometheus com /metrics em cada worker (cada um expondo na sua própria porta) e um scraper que agrega.
Armadilhas comuns
Estado em memória local ao worker
O que acontece: Cada worker tem sua própria heap — new Map() no início do módulo cria N maps independentes. Uma requisição que escreve no map do Worker 1 não é visível no Worker 2.
Por quê: Cluster usa child_process.fork() internamente — processos OS separados sem memória compartilhada. Não existe sincronização automática de estado in-process entre workers.
Como evitar: Session stores externos (Redis via connect-redis), bancos de dados, ou qualquer serviço stateful fora do processo Node. Nenhuma escrita crítica em Map/objeto global de worker.
// ❌ cada worker tem seu próprio Mapconst sessions = new Map();// ✓ store externoimport RedisStore from 'connect-redis';app.use(session({ store: new RedisStore({ client: redis }) }));
Esquecer o handler 'exit'
O que acontece: Um worker que crasha com exceção não capturada simplesmente some. O sistema continua operando com N-1 (ou menos) workers até reiniciar o processo inteiro — silenciosamente, sem alerta.
Por quê: O primary apenas observa o ciclo de vida dos workers; sem o handler 'exit', nenhuma ação é tomada quando um worker morre.
Como evitar: Sempre registrar cluster.on('exit', ...) e chamar cluster.fork() para substituir workers crashados.
// ❌ sem handler: worker crashado nunca é substituídoif (cluster.isPrimary) { for (let i = 0; i < numCPUs; i++) cluster.fork();}// ✓ com watchdogcluster.on('exit', (worker, code, signal) => { if (!worker.exitedAfterDisconnect) { console.warn(`Worker ${worker.id} crashou. Reiniciando.`); cluster.fork(); }});
Sticky sessions sem reverse proxy ciente
O que acontece: WebSocket vai para o Worker 1 no handshake, mas a próxima mensagem pode ir para o Worker 2 — que não tem a conexão aberta. A conexão cai imediatamente.
Por quê: Round-robin distribui por conexão TCP, não por cliente. WebSocket e SSE exigem que todas as mensagens de um cliente vão para o mesmo processo durante toda a sessão.
Como evitar: Usar @socket.io/sticky para roteamento sticky no primary, ou nginx com ip_hash na frente do cluster — cada worker escutando em porta diferente.
// ❌ round-robin quebra WebSockethttp.createServer((req, res) => { // round-robin do cluster não respeita afinidade de conexão WebSocket}).listen(3000);// ✓ nginx com ip_hash garante que o mesmo IP vai sempre para o mesmo upstream// upstream nodejs_cluster { ip_hash; server 127.0.0.1:3001; ... }
O que acontece: 4 réplicas de pod × 4 workers por pod = 16 processos Node, com overhead de memória e complexidade operacional sem ganho proporcional de throughput HTTP.
Por quê: K8s (e similares) já gerencia restart, health check e escalonamento horizontal. Cluster dentro de container duplica essa responsabilidade sem adicionar valor.
Como evitar: 1 processo Node por container — deixe o orquestrador escalar réplicas.
# ❌ Configuração problemática:
K8s: 4 réplicas de pod
Cada pod: cluster com 4 workers
Total: 16 processos Node
# ✓ O que funciona melhor:
K8s: 4 réplicas de pod
Cada pod: 1 processo Node
Total: 4 processos Node (mais simples, mesma capacidade de throughput HTTP)
Não diferenciar crash de graceful exit no handler 'exit'
O que acontece: Um fork incondicional reinicia workers que saíram intencionalmente durante um graceful shutdown, impedindo que o processo encerre corretamente — ou criando workers extras depois de um deploy.
Por quê: O evento 'exit' dispara tanto para crashes quanto para saídas intencionais. Sem distinção, o handler não sabe se deve reiniciar ou não.
Como evitar: Verificar worker.exitedAfterDisconnect antes de fazer cluster.fork(). Esse flag é true quando o worker saiu via disconnect() — ou seja, saída intencional.
// ❌ fork incondicional: problema durante shutdowncluster.on('exit', (worker) => { cluster.fork();});// ✓ verificar exitedAfterDisconnectcluster.on('exit', (worker, code, signal) => { if (!worker.exitedAfterDisconnect) { cluster.fork(); }});
Em entrevista
Frase pronta (inglês)
“The cluster module forks multiple worker processes that share the same HTTP port. The primary process manages the lifecycle — it forks workers, listens for exit events, and restarts crashed workers. On Linux, Node distributes incoming connections in round-robin across workers. The main limitations are stateful workloads — each worker has its own memory, so sessions and caches must live in external storage. In 2026, if you have an orchestrator like Kubernetes, you don’t need cluster at all — the orchestrator handles scaling horizontally. Cluster still makes sense for single-VM deployments or when you need fine-grained control over worker lifecycle.”
Vocabulário técnico
Português
Inglês
Contexto de uso
cluster
cluster
”Node’s cluster module forks worker processes”
processo primary
primary process
”the primary manages worker lifecycle”
processo worker
worker process
”each worker handles HTTP requests independently”
compartilhamento de porta
port sharing
”workers share the same port via fd inheritance”
sessão sticky
sticky session
”WebSockets require sticky sessions”
encerramento gracioso
graceful shutdown
”disconnect workers before killing the primary”
fork de processo
process fork
”cluster.fork() spawns a new worker”
round-robin
round-robin
”Linux uses round-robin to distribute connections”
afinidade de IP
IP affinity
”ip_hash ensures IP affinity in nginx”
Perguntas frequentes em entrevista
“Como cluster difere de Worker Threads?”
Cluster cria processos separados (memória isolada, overhead de ~30MB por worker, tolerante a falhas — worker crash não afeta o primary). Worker Threads cria threads dentro do mesmo processo (memória compartilhável via SharedArrayBuffer, menor overhead, crash pode afetar o processo inteiro).
“Como workers compartilham a porta?”
O primary cria o socket, e os workers herdam o file descriptor via IPC. No Linux com SCHED_RR, o primary aceita conexões e as despacha para workers em round-robin. No modo SCHED_NONE, cada worker aceita diretamente — com risco de distribuição desigual.
“O que acontece se um worker crasha?”
Sem handler, ele some silenciosamente. Com cluster.on('exit', ...), o primary detecta e pode fazer cluster.fork() para substituir. worker.exitedAfterDisconnect permite distinguir crash de saída intencional.
Rubric
Critério
Status
TL;DR claro e correto
ok
Código funcional e idiomático
ok
Mecanismo de port sharing explicado (Linux vs Windows)
ok
Graceful shutdown com exitedAfterDisconnect
ok
Sticky sessions: problema + 2 soluções
ok
Armadilhas com exemplos de código
ok
Vocabulário EN para entrevista
ok
Frase pronta em inglês
ok
Sem fabricação de dados do usuário
ok
Wikilinks para notas existentes e futuras
ok
Sem Co-Authored-By no commit
pendente
O que vem a seguir
Cluster resolve o problema de aproveitar múltiplos cores numa single-VM — mas quando sua aplicação crescer além de uma máquina, você vai precisar decidir entre PM2, Kubernetes e outros orquestradores. A nota [[10 - Cluster vs PM2 vs Kubernetes - quem orquestra]] explora essa decisão com profundidade: quando cada camada faz sentido, o erro clássico de rodar cluster dentro de um pod K8s, e como a evolução histórica das ferramentas explica as escolhas de 2026. Se você ainda não leu sobre sticky sessions com WebSocket em detalhe, a nota [[11 - Decision tree - qual ferramenta para qual problema]] tem o raciocínio completo de quando usar cada ferramenta, com cenários reais.