Armadilhas, regras práticas, cheatsheet

TL;DR

Nota de fechamento do galho 2. Top 10 armadilhas extraídas das notas 01–11, tabela ferramenta×atributo, decision tree compactada em 1 tela, vocabulário PT→EN consolidado do galho (19 termos), e próximos galhos recomendados (Streams, Observability, Segurança).


Top 10 armadilhas

1. Worker sem terminate em shutdown → leak de threads

Problema: Workers ativos impedem o processo de encerrar. Sem cleanup explícito no SIGTERM, threads ficam órfãs e o processo trava no shutdown.

// ❌ pool criado, nunca destruído
const pool = new Piscina({ filename: './worker.js' });

Fix:

process.on('SIGTERM', async () => {
  await pool.destroy();       // piscina
  // ou: await worker.terminate(); // Worker individual
  process.exit(0);
});

2. IPC leak — mensagens acumulando na fila quando child não lê → memory growth

Problema: O canal IPC entre pai e filho tem buffer. Se o pai envia mensagens mais rápido do que o filho as consome, a fila cresce sem limite. O processo cresce em memória silenciosamente.

// ❌ parent envia sem backpressure
setInterval(() => child.send(grandeBatch), 10);
// filho processa a 1/s — fila cresce 100x por segundo

Fix: implementar backpressure (aguardar drain ou confirmação do filho antes de enviar o próximo lote), ou fechar o canal após a troca de dados com child.disconnect().


3. Race condition em SharedArrayBuffer sem Atomics → corrupção de estado

Problema: Duas threads lendo e escrevendo no mesmo índice de um SAB sem coordenação. As operações view[0]++ não são atômicas — são três instruções de CPU. Com duas threads em paralelo, o resultado é imprevisível.

// ❌ não atômico — race condition garantida com 2+ workers
const view = new Int32Array(sab);
view[0]++;

Fix:

// ✓ sempre Atomics para leitura e escrita compartilhada
Atomics.add(view, 0, 1);
// ou: Atomics.load / Atomics.store / Atomics.compareExchange

4. exec com input do usuário → shell injection

Problema: child_process.exec sempre invoca um shell (/bin/sh). Input não sanitizado pode injetar comandos arbitrários.

// ❌ CVE esperando para acontecer
exec(`convert ${req.body.filename} output.png`);
// filename = 'x; rm -rf /'  → desastre

Fix:

// ✓ execFile ou spawn com array de args — sem shell, sem injeção
execFile('convert', [req.body.filename, 'output.png']);
// ou: spawn('convert', [req.body.filename, 'output.png'])

5. Cluster com estado em memória (cache local) → comportamento inconsistente entre workers

Problema: Cluster cria N processos independentes. Estado em memória (Map, objeto global, cache) existe separadamente em cada worker. Requisições para o mesmo endpoint chegam em workers diferentes — o estado nunca converge.

// ❌ cada worker tem seu próprio Map — 4 workers = 4 caches desconexos
const cache = new Map();
app.get('/item/:id', (req, res) => {
  if (cache.has(req.params.id)) return res.json(cache.get(req.params.id));
  // ...
});

Fix: estado compartilhado em camada externa — Redis, banco de dados, ou serviço dedicado. Nenhuma escrita crítica em memória de processo worker.


6. Fork sem cleanup de child em parent crash → processos zumbi

Problema: Se o processo pai crasha sem sinalizar os filhos, os filhos ficam órfãos — rodando sem supervisão, consumindo recursos, sem chance de encerramento gracioso.

// ❌ nenhum handler de cleanup no pai
const child = fork('./worker.js');
// pai crasha → child continua vivo indefinidamente

Fix:

// ✓ cleanup explícito em sinais do pai
process.on('SIGTERM', () => {
  child.kill('SIGTERM');
  process.exit(0);
});
process.on('exit', () => child.kill());

7. transferList esquecido em buffer grande → cópia silenciosa de bytes

Problema: postMessage(buf) sem [buf] no segundo argumento faz uma cópia completa do ArrayBuffer. Com buffers de imagem, áudio ou ML de dezenas de MB, o heap cresce o dobro por alguns milissegundos. Sem aviso em runtime — o código funciona, mas é lento.

// ❌ cópia silenciosa de 100 MB
worker.postMessage(imagemBuffer);

Fix:

// ✓ transferência zero-copy — original fica detached
worker.postMessage(imagemBuffer, [imagemBuffer]);

8. Worker preso em loop síncrono → mensagens não processadas, terminate é única saída

Problema: Um Worker em loop síncrono (while(true) ou cálculo sem pausa) não processa mensagens recebidas. O event loop do worker está travado. O pai não consegue encerrar cooperativamente — só terminate(), que é equivalente a SIGKILL.

// ❌ loop síncrono bloqueia o event loop do worker
while (true) {
  processarChunk(dados);
}
// parentPort.on('message') nunca é alcançado

Fix: dividir o trabalho em chunks com pausas que liberam o event loop:

// ✓ cede o event loop a cada chunk
async function processar(chunks) {
  for (const chunk of chunks) {
    processarChunk(chunk);
    await new Promise((r) => setImmediate(r)); // yield
  }
  parentPort.postMessage({ done: true });
}

9. Cluster + sticky sessions sem reverse proxy ciente → WebSocket quebra na troca de worker

Problema: WebSocket é uma conexão persistente. Se o reverse proxy não tem sticky sessions (affinity), requisições do mesmo cliente chegam em workers diferentes — e o handshake WebSocket não é reenviado. A conexão quebra ou fica em estado inválido.

Fix: configurar sticky sessions no reverse proxy:

# nginx — ip_hash como afinidade básica
upstream node_cluster {
  ip_hash;
  server 127.0.0.1:3001;
  server 127.0.0.1:3002;
  server 127.0.0.1:3003;
  server 127.0.0.1:3004;
}

Ou usar @socket.io/sticky para afinidade baseada em cookie/sid, que é mais precisa que ip_hash atrás de NAT.


10. spawn com shell: true e input do usuário → shell injection idêntica ao exec

Problema: spawn sem shell é seguro. Com shell: true, o comportamento é idêntico ao exec — a string inteira é passada para /bin/sh. Qualquer input de usuário na string vira vetor de injeção.

// ❌ shell: true anula a segurança do spawn
spawn('convert ' + req.body.filename + ' output.png', { shell: true });

Fix:

// ✓ sem shell, args como array — nunca shell: true com input externo
spawn('convert', [req.body.filename, 'output.png']);

Cheatsheet — 3 ferramentas × 5 atributos

AtributoWorker ThreadClusterchild_process
Modeloshared-memory (threads no mesmo processo)shared-port (multi-processo, mesma porta TCP)separate-process (isolamento total)
Custo de criação~1–5 ms por thread~100 ms por fork~100 ms por fork
IPC / ComunicaçãopostMessage / SharedArrayBuffer / transferListIPC built-in (worker.send / process.on('message'))stdio streams (spawn/exec) ou IPC (fork)
Use case principalCPU-bound dentro de um handler ou jobEscalar servidor HTTP em single-VM sem orquestradorRodar comando externo (ffmpeg, python, git) ou Node filho isolado
Lib de prodpiscina (pool de workers)PM2 em modo cluster (legacy) / orquestrador externo— (use execFile ou spawn diretamente)

Regra rápida

Worker Thread → CPU-bound. Cluster → HTTP scaling em single-VM. child_process → comando externo ou processo isolado.


Decision tree compactada

Qual o problema?
│
├─ CPU-bound em handler ou job?
│   └─ Worker Thread
│       ├─ Alta carga / frequente? → pool via piscina (availableParallelism() workers)
│       └─ Esporádico? → Worker por task é OK
│
├─ Escalar HTTP além de 1 thread?
│   ├─ Tem orquestrador (K8s, ECS, Fly.io)? → não adicionar Cluster; 1 processo por pod
│   └─ Single-VM sem orquestrador? → Cluster (ou PM2 em modo cluster)
│
├─ Rodar comando externo (ffmpeg, git, python, imagemagick)?
│   ├─ Output grande (> 1 MB) ou processo longo? → spawn (streams)
│   ├─ Output pequeno, comando hardcoded? → exec
│   └─ Args vêm de input externo? → SEMPRE execFile ou spawn com array; NUNCA exec
│
└─ Spawnar processo Node filho isolado?
    ├─ CPU-bound sem necessidade de isolamento? → Worker Thread (mais leve)
    ├─ Isolamento total / native module legado / código não-confiável? → fork
    └─ Supervisor tree / processo descartável? → fork + backoff exponencial

Antes de percorrer a árvore

  1. Medir: event loop lag, percentis de latência (p50/p95/p99), CPU por thread.
  2. Identificar: CPU-bound ou I/O-bound?
  3. Testar alternativas: streaming, paginação, refatoração, API async, UV_THREADPOOL_SIZE, fila de background.
  4. Só então: percorrer a decision tree.

Vocabulário PT→EN consolidado

PT-BREN
paralelismoparallelism
concorrênciaconcurrency
thread de trabalhoWorker Thread
porta-paiparentPort
bifurcarfork
spawnarspawn
pool de workersworker pool
memória compartilhadashared memory
operação atômicaatomic operation
condição de corridarace condition
comparar-e-trocarcompare-and-swap (CAS)
aguardar-notificarwait-notify
porta compartilhadashared port
comunicação interprocessointer-process communication (IPC)
injeção de shellshell injection
zumbizombie process
encerramento graciosograceful shutdown
orquestradororchestrator
réplicareplica

Próximos galhos

Galho 3 — Streams

Para dados grandes sem bloquear o event loop: Readable, Writable, Transform, backpressure. Quando JSON.parse de payload inteiro já é o gargalo e a solução é processar em chunks enquanto os bytes chegam.

Galho 5 — Observability

Para observar workers e cluster em produção: métricas de pool (fila, idle workers, throughput), profiling de Worker Threads (V8 CPU profiler, Clinic.js), alertas em event loop lag, rastreamento distribuído entre processos.

Galho 6 — Segurança

Para isolamento e sandbox: vm module, isolated-vm (V8 isolate sem acesso a APIs Node), Permission Model (Node 20+), execução de código não-confiável sem acesso ao sistema de arquivos ou rede.


Veja também