Nota de fechamento da sub-trilha index. Agrega as 10 armadilhas mais críticas (com exemplo e fix), tabela de timer/fase, decision tree de “minha request está lenta”, vocabulário PT→EN com 22 termos, e ponteiros para os próximos galhos. Use como referência rápida após percorrer as 11 notas anteriores. Regras de ouro: nunca use *Sync em handlers, nunca recorra em nextTick, meça event loop lag antes de otimizar, e trate latência conjunta diferente de latência isolada.
O Node.js pune o que parece inocente
Você escreve fs.readFileSync('./config.json') no topo de um handler porque é simples e “funciona em dev”. Funciona mesmo — até o primeiro deploy com 50 usuários simultâneos. Nesse momento, cada request bloqueia a thread JS lendo o arquivo, e as outras 49 esperam na fila.
Ou você usa process.nextTick num loop de processamento porque “é eficiente”. Funciona — até entrar em recursão e o event loop parar de avançar para sempre.
Esta nota agrega os padrões que aparecem repetidamente em incidents de produção Node.js: os que parecem inofensivos em isolamento mas escalam de forma desastrosa. Use como checklist de code review, onboarding de time, ou referência rápida durante um incident.
Top 10 armadilhas
1. Recursão em process.nextTick → starvation
nextTickQueue é drenada completamente antes de qualquer outra fase. Recursão nela impede o event loop de avançar para sempre.
Fix: use setImmediate — cede controle ao event loop após cada iteração.
function loop() { setImmediate(loop); // event loop avança entre cada chamada}
2. CPU-bound síncrono em handler async → bloqueia tudo
Código síncrono pesado na thread JS congela todos os endpoints enquanto roda. Não importa quantos await existem no handler: o trecho síncrono bloqueia.
3. Promise.all em lista grande sem limite → satura recursos
Promise.all dispara todas as promises ao mesmo tempo. Com listas grandes, isso abre centenas de conexões, esgota o thread pool ou sobrecarrega serviços externos.
// ERRADO — dispara 500 queries ao mesmo tempoconst resultados = await Promise.all(ids.map(id => buscarUsuario(id)));
Fix: use p-limit ou processe em batches com um loop serial controlado.
4. await sequencial onde paralelo cabe → lentidão escondida
await serial executa uma operação de cada vez. Se as operações são independentes, o tempo total é a soma dos tempos individuais — poderia ser o máximo.
// ERRADO — espera A terminar para começar Bconst usuario = await buscarUsuario(id);const pedidos = await buscarPedidos(id);
Fix:Promise.all quando as operações são independentes entre si.
Se o callback demora mais que o intervalo, o próximo disparo começa antes do anterior terminar. Os callbacks se acumulam e causam drift progressivo.
// ERRADO — se relatorio() demora >1s, os callbacks se acumulamsetInterval(() => gerarRelatorio(), 1000);
Fix:setTimeout recursivo — o próximo intervalo só começa após o callback terminar.
async function agendarRelatorio() { await gerarRelatorio(); setTimeout(agendarRelatorio, 1000); // próximo começa após terminar}agendarRelatorio();
6. unhandledRejection não tratado → processo termina silencioso
A partir do Node 15+, promises rejeitadas sem handler encerram o processo por padrão. Sem handler global, o encerramento pode acontecer sem log claro.
// ERRADO — rejeição silenciosa que mata o processo em Node 15+async function tarefa() { throw new Error('falhou');}tarefa(); // sem await, sem .catch()
Fix: handler global que loga antes de encerrar.
process.on('unhandledRejection', (reason, promise) => { console.error('Promise não tratada:', reason); process.exit(1);});
7. Sync APIs em handler de produção → trava o event loop
fs.readFileSync, crypto.pbkdf2Sync, JSON.parse de payloads grandes — qualquer operação síncrona demorada trava a thread JS para todas as requisições ativas.
// ERRADO — bloqueia o event loop enquanto o arquivo é lidoapp.get('/config', (req, res) => { const cfg = fs.readFileSync('./config.json', 'utf8'); // trava tudo res.json(JSON.parse(cfg));});
Fix: versão async + streaming para payloads grandes.
8. Thread pool exausto por I/O ou crypto concorrentes → timeouts
O thread pool do libuv tem apenas 4 threads por padrão. File I/O, dns.lookup, crypto e zlib concorrentes disputam essas 4 vagas. Quando todas estão ocupadas, novos pedidos esperam na fila — causando timeouts sem erro óbvio nos logs.
Fix: aumentar UV_THREADPOOL_SIZE ou mover para Worker Thread.
UV_THREADPOOL_SIZE=16 node server.js
9. Regex catastrófica em input do usuário → ReDoS
Certas expressões regulares têm backtracking exponencial quando o input não faz match. Um atacante pode travar a thread JS com um payload cuidadosamente construído.
// ERRADO — regex com backtracking catastrófico em input não controladoconst RE = /^(a+)+$/;RE.test('aaaaaaaaaaaaaaaaaaaaaaaab'); // pode levar segundos ou minutos
Fix: validar com biblioteca (zod/joi), limitar tamanho do input, usar regex sem backtracking excessivo.
import { z } from 'zod';const schema = z.string().max(100).regex(/^[a-z]+$/);schema.parse(req.body.campo); // valida e lança se inválido
10. Timer com closure pesado nunca limpo → memory leak
Closures capturadas por timers mantêm objetos no heap vivos. Se clearTimeout/clearInterval não for chamado no cleanup, os objetos nunca são coletados pelo GC.
// ERRADO — timer criado sem limpar; closure prende objeto grandefunction iniciar(dados) { const intervalo = setInterval(() => processar(dados), 5000); // intervalo nunca é limpo; dados fica preso no heap}
Fix: guardar referência e limpar no cleanup (evento de desconexão, shutdown, etc.).
function iniciar(dados) { const intervalo = setInterval(() => processar(dados), 5000); return () => clearInterval(intervalo); // retorna função de cleanup}
Cheatsheet — timer e fase
API
Tipo
Fase do event loop
Quando usar
process.nextTick
microtask (nextTickQueue)
entre fases (prioridade máxima)
deferir mínimo; prioridade acima de Promises; evitar recursão
queueMicrotask
microtask (microtask queue)
entre fases
padrão portável (web/Bun/Deno); após código síncrono
Promise.then
microtask (microtask queue)
entre fases
após uma promise resolver; mesmo nível que queueMicrotask
setTimeout(fn, ms)
macrotask
timers
delay com tempo mínimo; ms=0 efetivamente 1ms
setInterval(fn, ms)
macrotask
timers
repetição periódica; preferir setTimeout recursivo em produção
setImmediate(fn)
macrotask
check
após I/O da iteração atual; antes do próximo timer
Ordem de prioridade em cada ponto de drenagem:
nextTickQueue (toda) → microtask queue (toda) → próxima fase do event loop
flowchart TD
START["Latência elevada detectada"] --> Q1{"Lentidão é conjunta?\n(todos os endpoints)"}
Q1 -->|"SIM"| EL["Event loop bloqueado\n(ver nota 10 e 11)"]
Q1 -->|"NÃO"| ISO["Lentidão isolada\n(endpoint específico)"]
EL --> Q2{"Qual categoria?"}
Q2 -->|"CPU síncrono\n(regex, JSON.parse, loop)"| FIX1["Worker Thread /\nstreaming / paginação"]
Q2 -->|"Sync API no handler\n(*Sync, execSync)"| FIX2["Versão async\nequivalente"]
Q2 -->|"fs/crypto/dns concorrente\n(> 4 simultâneos)"| FIX3["UV_THREADPOOL_SIZE ↑\nou Worker Thread"]
Q2 -->|"GC pause\n(heap crescendo)"| FIX4["--inspect + flame chart\nprofiling de heap"]
ISO --> Q3{"P50 alto ou P99?"}
Q3 -->|"P50 alto"| ISO1["Lógica lenta\nDB query / await serial\nFix: Promise.all / índice"]
Q3 -->|"P99 alto, P50 ok"| ISO2["Race condition /\npool cheio\nFix: pool size / lock"]
Vocabulário PT→EN
Compilado de toda a sub-trilha. Mínimo necessário para entrevistas internacionais em inglês.
Termo PT
Termo EN
Nota de contexto
loop de eventos
event loop
mecanismo central do Node; ciclo de fases do libuv
thread única
single thread
única thread que executa código JS
I/O não-bloqueante
non-blocking I/O
chamadas retornam imediatamente; callback notifica quando pronto
microtarefa
microtask
executa entre fases; nextTick, queueMicrotask, Promise.then
macrotarefa
macrotask
agendada numa fase; setTimeout, setInterval, setImmediate
esgotamento de fila
queue starvation
recursão em nextTick impede avanço do event loop
pool de threads
thread pool
4 threads libuv para fs, crypto, dns.lookup, zlib
async no kernel
kernel async I/O
epoll/kqueue/IOCP — rede não consome threads
epoll / kqueue / IOCP
epoll / kqueue / IOCP
mecanismos de polling assíncrono de I/O no Linux/macOS/Windows
aguardar
await
pausa a função async; libera a thread JS durante a espera
promise liquidada
Promise settled
estado final: fulfilled ou rejected; imutável
iterador assíncrono
async iterator
for await...of; consome streams/geradores async
atraso do event loop
event loop lag
atraso entre tick planejado e tick real; indica bloqueio
gráfico de chamas
flame chart
visualização de CPU profile; eixo X = tempo, eixo Y = call stack
negação de serviço por regex
ReDoS
ataque via regex com backtracking catastrófico
backtracking catastrófico
catastrophic backtracking
complexidade exponencial em regex com alternativas sobrepostas
ligado à CPU
CPU-bound
workload onde o gargalo é processamento, não I/O
ligado a I/O
I/O-bound
workload onde o gargalo é disco/rede/banco
fila de callbacks
callback queue
fila geral de macrotasks pendentes
desvio de timer
timer drift
acúmulo de atraso progressivo em setInterval reentrante
coletor de lixo
garbage collector (GC)
V8 gerencia heap; pausa a thread em certas fases
histograma
histogram
estrutura de dados para percentis de latência (HdrHistogram)
Casos práticos
Cenário 1 — Code review bloqueou deploy: 3 armadilhas no mesmo handler
Durante code review de um feature de geração de relatórios, 3 problemas foram identificados no mesmo handler antes do merge:
// ❌ Handler original — 3 armadilhas em 15 linhasapp.post('/relatorio', async (req, res) => { const config = fs.readFileSync('./config.json'); // armadilha 7: sync API const dados = await buscarDados(req.body.filtros); // armadilha 4: await serial em operações independentes const usuarios = await buscarUsuarios(dados.ids); const produtos = await buscarProdutos(dados.skus); // armadilha 3: Promise.all sem limite em lista potencialmente grande const detalhes = await Promise.all( dados.ids.map(id => buscarDetalheCompleto(id)) // pode ser 500 ids ); res.json({ config, usuarios, produtos, detalhes });});// ✅ Handler corrigidoimport pLimit from 'p-limit';const limit = pLimit(10);// Carregar config uma vez no startup, não por requestconst config = JSON.parse(await fs.promises.readFile('./config.json', 'utf8'));app.post('/relatorio', async (req, res) => { const dados = await buscarDados(req.body.filtros); // Paralelo onde seguro const [usuarios, produtos] = await Promise.all([ buscarUsuarios(dados.ids), buscarProdutos(dados.skus), ]); // Concorrência limitada para lista grande const detalhes = await Promise.all( dados.ids.map(id => limit(() => buscarDetalheCompleto(id))) ); res.json({ config, usuarios, produtos, detalhes });});
Cenário 2 — Cheatsheet de timer usado em onboarding de dev júnior
Em um onboarding, um dev júnior perguntou: “quando devo usar setImmediate em vez de setTimeout(fn, 0)?“. A resposta veio pela tabela de timer/fase desta nota:
Dentro de callback de I/O: setImmediate é determinístico — dispara na fase check da iteração atual.
setTimeout(fn, 0) depende da fase timers — pode ou não ser antes do setImmediate dependendo do contexto.
Para yield do event loop (ceder entre chunks de processamento): setImmediate é o padrão seguro.
Para delay mínimo garantido (N ms): setTimeout.
Três semanas depois, o mesmo dev identificou um setInterval reentrante num serviço de relatórios e aplicou o fix do item 5 da lista de armadilhas.
Armadilhas comuns
O mais silencioso: closures em timers não limpos → memory leak progressivo
Timers que capturam closures com objetos grandes (arrays, buffers, conexões) e nunca são limpos mantêm esses objetos vivos no heap indefinidamente. O GC não consegue coletar porque a referência existe no closure. Sintoma: RSS do processo crescendo lentamente sem OutOfMemory imediato.
// ❌ closure captura `dados` (pode ser MB); intervalo nunca é limposetInterval(() => processar(dados), 1000);// ✅ retornar cleanup — obrigatório em módulos com lifecycleconst id = setInterval(() => processar(dados), 1000);return () => clearInterval(id);
process.nextTick recursivo → starvation completo do event loop
nextTickQueue drena inteira antes de qualquer fase do loop avançar. Recursão nela é um loop infinito dentro do próprio mecanismo de drenagem — o loop para de processar I/O, timers, e qualquer outro evento.
// ❌ Event loop congelafunction loop() { process.nextTick(loop); }loop();// ✅ setImmediate cede uma iteração do loop por chamadafunction loop() { setImmediate(loop); }
await serial é O(n) — use Promise.all para operações independentes
Cada await pausa a função até a promise resolver antes de iniciar a próxima. Para N operações independentes com duração D, o tempo total é N×D. Promise.all inicia todas ao mesmo tempo; o tempo total é max(D₁, D₂, …, Dₙ).
O anti-pattern é especialmente perigoso quando mascarado por um for...of — parece explícito mas na prática é sequencial: for (const id of ids) await buscar(id).
Como explicar em inglês
Frase pronta (EN)
“Node.js has one JavaScript thread, so any synchronous code — CPU-bound work, sync I/O APIs, catastrophic regex — blocks every other request while it runs. The key distinction to make in an incident is: correlated latency across all endpoints points to an event loop block; isolated latency on one endpoint points to a problem in that handler specifically. The rule of thumb: never use *Sync APIs in request handlers, never recurse in process.nextTick, and always measure event loop lag before assuming a performance problem is in the database.”
Vocabulário PT↔EN
PT-BR
EN
loop de eventos
event loop
microtarefa
microtask
macrotarefa
macrotask
fila de nextTick
nextTick queue
pool de threads
thread pool
bloqueio do loop
event loop blocking
starvation de fila
queue starvation
desvio de timer
timer drift
latência conjunta
correlated latency
ponteiro de limpeza
cleanup reference
O que vem a seguir
Esta nota fecha o galho de Runtime e Event Loop. Os próximos galhos constroem sobre o que você aprendeu aqui:
Galho 2 — Paralelismo: quando o código síncronamente pesado não tem alternativa async — Worker Threads, cluster, child_process. A solução estrutural para CPU-bound.
Galho 3 — Streams: quando o dado é grande demais para caber em memória. A solução estrutural para JSON.parse de payloads grandes e I/O de alto volume.
Galho 5 — Observability: instrumentação permanente em produção — profiling contínuo, alertas de event loop lag, distributed tracing com OpenTelemetry.
Galho 2 — Paralelismo (quando o bloqueio é estrutural)
Quando o problema é CPU-bound e não pode ser resolvido com async/await: Worker Threads, cluster e child_process. Worker Threads permite JS verdadeiramente paralelo em múltiplas threads dentro do mesmo processo.
Acesse quando: CPU-bound inevitável, processamento de imagem, criptografia pesada, parsing de arquivos grandes.
Wikilink: [[Paralelismo]] (a criar)
Galho 3 — Streams (quando o dado é grande)
Para processar dados grandes sem carregar tudo na memória e sem bloquear: Streams Node.js. Readable, Writable, Transform, pipeline.
Acesse quando: upload/download de arquivos, parsing de CSV/JSON grandes, proxying de dados, compressão on-the-fly.
Wikilink: [[Streams]] (a criar)
Galho 5 — Observability (quando você precisa enxergar em produção)