O que é o event loop?
O event loop é o mecanismo que permite ao Node.js processar múltiplas operações de I/O de forma não-bloqueante, mesmo rodando em uma única thread JavaScript.
A ideia central: o event loop processa um callback por vez, mas não precisa esperar I/O terminar para começar o próximo.
Quando você faz uma query ao banco, o Node.js registra um callback, devolve o controle ao event loop, e só executa o callback quando o banco responde. Nesse meio tempo, outros requests são atendidos normalmente.
Node.js tem mais threads do que você imagina
O que é single-threaded é a thread do event loop: onde seu JavaScript executa. Por baixo, a libuv (biblioteca C responsável pelo I/O) usa um thread pool para operações de filesystem, DNS, e criptografia.
Seu código JS ──→ Event Loop (1 thread JavaScript)
│
├── Rede (epoll/kqueue: I/O assíncrono do kernel)
├── Filesystem (thread pool libuv, padrão: 4 threads)
├── DNS (thread pool libuv)
└── Crypto (thread pool libuv)
Quando você chama fs.readFile, a leitura acontece numa thread da libuv. Quando termina, o callback é colocado na fila do event loop para execução. O event loop em si nunca bloqueou.
As fases do event loop
O event loop não é uma fila simples: tem fases distintas, executadas em ordem:
timers → setTimeout, setInterval
pending I/O → callbacks de I/O com erro do tick anterior
idle/prepare → uso interno do Node.js
poll → aguarda I/O; executa callbacks de I/O prontos
check → setImmediate
close → socket.on('close', ...)
Entre cada fase, o Node.js drena a fila de microtasks: process.nextTick e Promises resolvidas executam antes de passar para a próxima fase.
Isso explica comportamentos como:
Promise.resolve().then(() => console.log("A"));
process.nextTick(() => console.log("B"));
setImmediate(() => console.log("C"));
setTimeout(() => console.log("D"), 0);
// Saída: B → A → D → C
// nextTick > Promises > setTimeout ≈ setImmediateO que significa "bloquear o event loop"
Bloquear o event loop significa executar código JavaScript síncrono por tempo suficiente para atrasar todos os outros callbacks na fila.
Durante uma operação síncrona, o event loop não pode processar nada mais. Nenhum request entra, nenhuma resposta sai, nenhum callback de I/O executa.
// Isso bloqueia por ~200ms em produção:
app.get("/report", (req, res) => {
const data = loadMillionsOfRows(); // síncrono
const result = processData(data); // CPU-intensivo
res.json(result);
});Durante esses 200ms, todos os outros clients ficam na fila, esperando. Mesmo que a request deles fosse trivial.
Como medir o lag do event loop
import { performance } from "perf_hooks";
function measureEventLoopLag() {
const start = performance.now();
setImmediate(() => {
const lag = performance.now() - start;
if (lag > 10) {
// Mais de 10ms de lag é um sinal de atenção
console.warn(`Event loop lag: ${lag.toFixed(2)}ms`);
}
});
}
setInterval(measureEventLoopLag, 1000);A lógica: setImmediate deveria executar quase imediatamente após ser registrado. Se demorar 100ms, significa que o event loop ficou ocupado por esses 100ms.
Para análise profunda, use clinic:
npx clinic doctor -- node server.js
npx clinic flame -- node server.jsAs causas mais comuns de bloqueio
1. JSON pesado
// 10MB de JSON → ~50ms de bloqueio
const obj = JSON.parse(largeJsonString);
const str = JSON.stringify(largeObject);Se você processa payloads grandes com frequência, considere streaming JSON (stream-json) ou mover o processamento para um Worker Thread.
2. Criptografia síncrona
// NÃO FAÇA:
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, "sha512");
// pbkdf2Sync com 100k iterações → 200-500ms de bloqueio
// FAÇA:
const hash = await new Promise<Buffer>((resolve, reject) => {
crypto.pbkdf2(password, salt, 100000, 64, "sha512", (err, key) => {
err ? reject(err) : resolve(key);
});
});A versão assíncrona usa o thread pool da libuv: o event loop fica livre.
3. Expressões regulares complexas (ReDoS)
// Perigoso com input não validado:
const emailRegex = /^([a-zA-Z0-9])+([a-zA-Z0-9._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9._-]+)+$/;
emailRegex.test(maliciousInput); // pode travar por segundosSempre valide o tamanho do input antes de aplicar regex complexa. Use bibliotecas como Zod para validação de email: elas têm implementações seguras.
4. Loops síncronos sobre grandes coleções
// Se items tem 500k elementos, isso bloqueia por segundos:
const results = items.map((item) => heavyTransform(item));Quebre em chunks com setImmediate para ceder o event loop entre eles, ou mova para Worker Threads.
5. Variantes *Sync de I/O
// NUNCA em produção:
const content = fs.readFileSync("./config.json", "utf-8");
const result = execSync("ls -la");As versões *Sync bloqueiam o event loop inteiro durante a operação de I/O.
Worker Threads: quando e como usar
Para trabalho CPU-bound que não pode ser evitado, Worker Threads permitem rodar JavaScript em threads separadas:
import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
if (isMainThread) {
function runInWorker(data: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: data });
worker.on("message", resolve);
worker.on("error", reject);
});
}
app.get("/process", async (req, res) => {
// Processamento pesado não bloqueia o event loop principal
const result = await runInWorker(req.body);
res.json(result);
});
} else {
// Executa na Worker Thread
const result = heavyCpuWork(workerData);
parentPort?.postMessage(result);
}Worker Threads têm overhead de criação: em produção, use um pool (biblioteca piscina).
I/O assíncrono: não é o problema
É importante entender: operações de I/O não bloqueiam o event loop:
// Isso NÃO bloqueia outros requests:
app.get("/users", async (req, res) => {
const users = await db.findMany(); // event loop fica livre durante a query
res.json(users);
});O await aqui não significa "esperar bloqueado". Significa "suspender esse callback, deixar o event loop processar outros callbacks, e retomar quando o resultado estiver pronto".
O event loop só é bloqueado por código JavaScript síncrono em execução.
Checklist para não bloquear
- Nunca use variantes
*Syncem produção (exceto na inicialização da aplicação) - Nunca use
pbkdf2Sync,scryptSync,randomFillSyncno caminho de requests - JSON de mais de 1-2MB merece atenção especial
- Sempre valide o tamanho do input antes de aplicar regex
- Loops sobre coleções grandes: meça, considere Worker Threads
- Use
clinicou0xperiodicamente para identificar hotspots de CPU
Conclusão
O event loop é elegante e altamente eficiente para workloads I/O-bound. O problema aparece quando você mistura trabalho CPU-bound: processar JSON pesado, criptografia síncrona, algoritmos complexos: com o servidor de requests.
A regra prática: se uma operação ocupa mais de ~1ms de CPU no caminho de um request, ela provavelmente não deveria estar no event loop principal. Meça primeiro, otimize onde importa.