Node.js Internals

Node.js Event Loop

O Node executa seu JavaScript em uma única thread, mas consegue lidar com milhares de conexões. O event loop e suas fases são o motivo disso funcionar — e por que bloqueá-lo é prejudicial.

intermediate15 min readUpdated 15 de set. de 2026
order.js
js
// order.js
console.log("1: synchronous");

setTimeout(() => console.log("5: timers phase"), 0);
setImmediate(() => console.log("6: check phase"));

Promise.resolve().then(() => console.log("3: microtask"));

process.nextTick(() => console.log("2: nextTick"));
Threads
Uma thread principal
Engine
V8 mais libuv
Fases
Timers, poll, check, close
Microtasks
nextTick e promises
Thread pool
Tamanho padrão 4
Bloqueio
Trava tudo

Por que importa

Por que o event loop é importante

Uma thread, muitas conexões

O loop multiplexa milhares de sockets em uma única thread ao nunca esperar de forma síncrona.

Ordenação previsível

Conhecer as fases e as filas permite raciocinar sobre quando os callbacks serão executados.

Bloqueio é o inimigo

Qualquer tarefa síncrona longa congela todas as outras requisições, por isso tarefas pesadas são movidas para fora do loop.

O panorama completo

As três partes do loop

As fases decidem o que é executado e quando, as microtasks rodam entre as fases, e o thread pool lida com o trabalho que não pode ser não-bloqueante.

Fases

Agendamento

O loop alterna entre timers, callbacks pendentes, poll, check e callbacks de fechamento (close).

Filas

Priorização

As microtasks são esvaziadas entre as fases, e o nextTick executa antes dos callbacks de promises.

Thread pool

Descarregamento

Trabalhos de arquivo, DNS e criptografia rodam no thread pool da libuv para que o loop permaneça livre.

O event loop em resumo

As ideias centrais

Fase de Timers

Executa callbacks agendados por setTimeout e setInterval que já venceram.

Fase de Poll

Recupera novos eventos de I/O e executa seus callbacks, bloqueando quando está ocioso.

Fase de Check

Executa callbacks de setImmediate logo após a fase de poll.

Microtasks

Callbacks de process.nextTick e promises executam entre as fases.

Thread pool

A libuv executa fs, DNS e algumas operações de criptografia em worker threads.

Monitoramento

Monitore o loop delay e a utilização do event loop para identificar bloqueios.

Uma breve historia

De servidores bloqueantes a I/O orientado a eventos

  1. 2009

    Node orientado a eventos

    O Node combina V8 com libuv para executar JavaScript com I/O não-bloqueante.

    09
  2. 2011

    Extração da libuv

    O event loop e as abstrações de plataforma tornam-se uma biblioteca independente.

    11
  3. 2015

    Promises e microtasks

    Promises nativas adicionam uma nova fila com regras de ordenação claras.

    15
  4. 2020

    Melhores diagnósticos

    Ferramentas como a utilização do event loop tornam o bloqueio mensurável.

    20
  5. Hoje

    O mesmo modelo em todo lugar

    O event loop sustenta servidores, ferramentas, runtimes serverless e até navegadores.

    Hoje

O guia completo

Node.js Event Loop: Tudo que voce precisa saber

Por que o event loop é importante

O Node.js executa seu JavaScript em uma única thread principal, mas um único servidor consegue lidar com milhares de conexões simultâneas. Isso é possível porque o event loop nunca espera. Quando uma operação seria bloqueante, o Node a entrega ao sistema operacional ou a um thread pool e continua com outras tarefas. Quando o resultado está pronto, um callback é colocado na fila e o loop o executa.

Entender o loop explica muito do comportamento do Node: por que a leitura síncrona de um arquivo congela todo o servidor, por que uma promise gera um log antes de um timer com atraso zero e por que tarefas pesadas de CPU precisam de um worker. Este é o modelo mental mais importante para escrever aplicações Node rápidas.

O loop e suas fases

O event loop é um ciclo de fases. Cada fase possui uma fila de callbacks, e o loop as processa antes de prosseguir.

  1. Timers — callbacks agendados por setTimeout e setInterval que já venceram.
  2. Pending callbacks — callbacks de sistema adiados, como alguns erros de TCP.
  3. Idle, prepare — manutenção interna.
  4. Poll — recupera novos eventos de I/O e executa seus callbacks. Se nada estiver pronto, ele pode bloquear brevemente aqui.
  5. Check — executa callbacks de setImmediate.
  6. Close callbacks — lida com eventos como o fechamento de sockets.

O loop continua ciclando enquanto houver trabalho a ser feito. Quando não há mais timers, nenhum I/O pendente e nenhum handle, o processo é encerrado.

Microtasks: nextTick e promises

Entre as fases, e após cada callback, o Node esvazia duas filas de alta prioridade:

  • Callbacks de process.nextTick, que são executados primeiro.
  • Callbacks de Promise, que são executados após o nextTick.

Ambas são completamente esvaziadas antes que o loop avance, o que gera duas consequências. Primeiro, elas rodam antes de timers e callbacks de I/O agendados anteriormente. Segundo, um nextTick recursivo ou uma cadeia de promises infinita pode causar starvation no loop, impedindo que o I/O seja executado.

// order.js
console.log("1: synchronous");

setTimeout(() => console.log("5: timers"), 0);
setImmediate(() => console.log("6: check"));

Promise.resolve().then(() => console.log("3: promise"));
process.nextTick(() => console.log("2: nextTick"));

A saída é 1, 2, 3, depois 5 e 6. A ordem de 5 e 6 pode variar no nível superior (top level) porque depende de quanto tempo leva para chegar à fase de timers; dentro de um callback de I/O, setImmediate sempre roda antes de setTimeout.

O thread pool do libuv

Algumas operações não podem ser realizadas de forma assíncrona pelo SO de maneira portátil: acesso ao sistema de arquivos, consultas de DNS e algumas funções criptográficas. O libuv executa essas tarefas em um thread pool, com o padrão de quatro threads, configurável via UV_THREADPOOL_SIZE.

É por isso que fs.readFile não bloqueia o seu código, embora não seja orientado a eventos internamente. Isso também significa que um pico de operações de arquivo pode ficar na fila atrás de um pool de quatro threads. As APIs async mantêm a thread principal livre; o pool cuida da espera.

Bloqueando o event loop

Qualquer operação síncrona e lenta na thread principal bloqueia tudo: outras requisições, timers e I/O. Os culpados mais comuns incluem:

  • readFileSync e outras chamadas de arquivo síncronas em request handlers.
  • JSON.parse ou JSON.stringify extensos em payloads grandes.
  • Criptografia síncrona, como pbkdf2Sync com alto número de iterações.
  • Loops intensos, backtracking de regex e transformações pesadas de dados.
  • Bibliotecas bloqueantes que realizam processamento de CPU na thread principal.

Meça em vez de adivinhar: o event loop delay e o event loop utilization indicam quanto tempo o loop permanece bloqueado. Um aumento no delay sob carga é o sinal claro.

Tirando o trabalho do loop

Quando o trabalho é genuinamente CPU-bound, mova-o:

  • worker_threads para computação no mesmo processo, utilizando a passagem de mensagens.
  • child processes para tarefas mais pesadas ou isoladas.
  • Um serviço separado para as cargas de trabalho mais pesadas, escalando-as de forma independente.
  • Chunking: divida tarefas grandes em pedaços menores e utilize o yield entre eles.

Para I/O, utilize as APIs async e streams para que o loop permaneça livre.

Melhores práticas

  • Use APIs async em request handlers; nunca bloqueie o loop.
  • Ceda o controle para I/O com setImmediate em vez de nextTick recursivos.
  • Mova tarefas CPU-bound para worker threads ou child processes.
  • Faça stream de grandes volumes de dados em vez de fazer o buffer de tudo na memória.
  • Ajuste o UV_THREADPOOL_SIZE apenas após realizar medições.
  • Monitore o event loop delay e a utilização em produção.
  • Mantenha as cadeias de callback curtas para que as microtasks não causem starvation de I/O.

Erros comuns

  • Usar readFileSync dentro de um hot path.
  • process.nextTick recursivos que consomem todos os recursos do loop.
  • Assumir que o Node.js é totalmente single-threaded e ignorar o thread pool.
  • Realizar computações pesadas inline em vez de delegá-las.
  • Esperar que setTimeout(fn, 0) seja executado antes de callbacks de I/O.
  • Ignorar o atraso do event loop até que problemas de latência apareçam.

Próximos passos

O event loop é o coração do Node.js. Continue com Streams para I/O em pedaços (chunked) e File System para acesso assíncrono a arquivos. Reveja os Fundamentos do Node.js se o runtime ainda for novo para você e, em seguida, meça o atraso do event loop em um serviço que você execute.

Lendo um arquivo

Trabalhos assíncronos rodam no thread pool e liberam o loop. Uma chamada síncrona bloqueia todas as outras requisições enquanto o disco responde.

Preferir
import { readFile } from "node:fs/promises";

const data = await readFile("big.log", "utf8");
res.end(data);
Evitar
import { readFileSync } from "node:fs";

// blocks the entire server
const data = readFileSync("big.log", "utf8");
res.end(data);

Agendando trabalho

nextTick executa antes dos callbacks de promises e antes do loop continuar. setImmediate executa na fase de check após o I/O.

Preferir
// after current I/O, don't starve
setImmediate(() => processBatch());
Evitar
// recursive nextTick starves
// the loop and blocks I/O
process.nextTick(() => loop());

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender Event Loop?

Nosso tutorial interativo te guia por Event Loop passo a passo — com quizzes e codigo real que voce pode executar no navegador.