Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Event Loop no Node.js: Guia Prático

Atualizado em: 8 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

O Node.js consegue atender muitas conexões simultâneas sem criar uma thread JavaScript para cada requisição. A peça central desse modelo é o Event Loop no Node.js, responsável por coordenar callbacks, timers, operações de entrada e saída e tarefas que retornam de recursos do sistema operacional.

Entender o event loop ajuda a explicar por que uma API pode ter ótimo desempenho com milhares de conexões e, ao mesmo tempo, ficar completamente travada por um único cálculo síncrono. Também esclarece a ordem entre Promises, process.nextTick(), timers e setImmediate().

Neste guia, você aprenderá as fases do event loop, a diferença entre tarefas e microtarefas, como a libuv participa do processamento, quais operações bloqueiam a thread principal e como medir atrasos em produção.

O que é o Event Loop?

O event loop é um mecanismo que verifica continuamente se existem callbacks prontos para execução. O JavaScript de uma aplicação Node.js normalmente roda em uma única thread. Operações de rede, sistema de arquivos, DNS e outras tarefas podem ser delegadas ao sistema operacional ou ao pool de threads da libuv.

Quando uma operação termina, seu callback é colocado na fila adequada. A thread principal executa esse callback quando termina o trabalho atual e alcança a fase correspondente.

A documentação oficial sobre o event loop apresenta o fluxo geral. Para revisar a plataforma, veja o que é Node.js e o que é JavaScript.

JavaScript síncrono continua sendo bloqueante

O modelo orientado a eventos não torna todo código automaticamente assíncrono. Um loop longo ocupa a thread principal:

const startedAt = Date.now();

while (Date.now() - startedAt < 5000) {
  // Trabalho síncrono intenso
}

console.log('Terminou');

Durante cinco segundos, nenhum callback de requisição, timer ou Promise pode executar. O processo continua ativo, mas deixa de responder de forma útil.

As principais fases do event loop

De forma simplificada, o ciclo percorre fases como:

  • timers: processa callbacks de setTimeout() e setInterval() prontos;
  • pending callbacks: executa determinados callbacks de operações do sistema;
  • idle e prepare: usadas internamente;
  • poll: recebe novos eventos de I/O e processa callbacks relacionados;
  • check: executa callbacks de setImmediate();
  • close callbacks: processa encerramentos, como o evento close de sockets.

Essa descrição é uma visão conceitual. Detalhes podem mudar entre versões do runtime, portanto não dependa de ordens que não fazem parte do contrato público.

Call stack

A call stack representa as funções JavaScript em execução. Enquanto existe uma função ativa na pilha, o event loop não pode executar outro callback:

function third() {
  console.log('terceira');
}

function second() {
  third();
}

function first() {
  second();
}

first();

As funções entram e saem da pilha em ordem. Apenas depois que a pilha fica vazia o runtime pode avançar para callbacks assíncronos.

Timers definem atraso mínimo

setTimeout(() => {
  console.log('timer');
}, 100);

O callback não é garantido exatamente aos 100 milissegundos. Ele se torna elegível depois desse prazo. Se a thread estiver ocupada, será executado mais tarde. O guia de Timers no Node.js aprofunda cancelamento, intervalos e APIs de Promises.

Microtarefas e Promises

Callbacks registrados com Promise.then(), catch(), finally() e queueMicrotask() entram na fila de microtarefas:

console.log('A');

Promise.resolve().then(() => {
  console.log('B');
});

console.log('C');

A saída será A, C e B. O callback da Promise executa depois que a pilha atual fica vazia, antes de o event loop avançar normalmente para outra fase.

process.nextTick()

process.nextTick() agenda uma continuação com prioridade especial no Node.js:

process.nextTick(() => {
  console.log('next tick');
});

Ele é útil para manter uma API assíncrona de forma consistente, mas pode causar starvation. Se cada callback registrar outro nextTick, timers e I/O podem ficar sem oportunidade de executar.

function repeatForever() {
  process.nextTick(repeatForever);
}

repeatForever();

Esse padrão deve ser evitado.

setImmediate() e setTimeout(0)

setImmediate() executa na fase check, enquanto um timeout de zero passa pela fase timers:

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

A ordem pode variar quando o código é executado no escopo principal. Dentro de um callback de I/O, setImmediate() normalmente é processado antes do próximo ciclo de timers. Use cada API por sua finalidade, não para forçar uma corrida de ordem.

O papel da libuv

A libuv fornece a implementação multiplataforma do event loop, comunicação com o sistema operacional e um pool de threads. Nem toda operação assíncrona utiliza o pool. Sockets de rede normalmente dependem de mecanismos eficientes do sistema operacional, enquanto algumas operações de arquivos, criptografia e DNS podem usar threads auxiliares.

O tamanho padrão do pool é limitado. Muitas operações pesadas que dependem dele podem criar fila mesmo quando a thread JavaScript está livre.

Thread pool e UV_THREADPOOL_SIZE

A variável UV_THREADPOOL_SIZE pode ajustar a quantidade de threads para operações compatíveis:

UV_THREADPOOL_SIZE=8 node server.js

Aumentar o valor não garante melhoria. Mais threads consomem memória, disputam CPU e podem apenas transferir o gargalo. Meça com uma carga representativa antes de alterar.

Operações que bloqueiam

Alguns exemplos comuns:

  • loops grandes e transformações intensivas em JavaScript;
  • expressões regulares vulneráveis a backtracking excessivo;
  • serialização de objetos enormes;
  • métodos síncronos de arquivo, criptografia ou compressão;
  • parsing de payloads sem limite;
  • callbacks que processam milhares de itens de uma vez.

Evite métodos com sufixo Sync durante o atendimento de requisições. Eles podem ser aceitáveis na inicialização ou em scripts curtos, desde que o bloqueio seja consciente.

Dividindo um trabalho grande

Uma tarefa pode ser dividida em pequenos lotes:

function processItems(items, index = 0) {
  const end = Math.min(index + 100, items.length);

  for (let current = index; current < end; current += 1) {
    transform(items[current]);
  }

  if (end < items.length) {
    setImmediate(() => processItems(items, end));
  }
}

Esse padrão permite que I/O e outros callbacks progridam entre os lotes. Para cálculo realmente intensivo, considere Worker Threads no Node.js.

Monitorando o event loop

O módulo node:perf_hooks oferece recursos para medir atrasos:

const { monitorEventLoopDelay } = require('node:perf_hooks');

const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

setInterval(() => {
  console.log({
    meanMs: histogram.mean / 1e6,
    maxMs: histogram.max / 1e6
  });

  histogram.reset();
}, 10000);

Monitore percentis e picos, não apenas a média. Um pequeno grupo de atrasos extremos pode prejudicar requisições sensíveis.

Event loop utilization

A utilização indica quanto tempo o loop permaneceu ativo em relação ao período observado. Valores altos durante muito tempo podem revelar saturação da thread principal. Compare essa métrica com CPU, latência, throughput, memória e duração das operações externas.

Backpressure

Mesmo um callback rápido pode causar problemas se dados chegarem mais depressa do que são processados. Streams oferecem backpressure para limitar a produção. O artigo sobre Streams no Node.js explica como respeitar write(), drain e pipeline().

APIs e limites

Em servidores HTTP, limite corpo de requisição, complexidade de filtros, quantidade de itens e tempo de processamento. Um atacante não precisa explorar uma falha sofisticada se puder enviar uma entrada que monopoliza a thread.

Para operações lentas, use timeout e cancelamento real com AbortSignal. Rejeitar uma Promise sem parar o trabalho não libera necessariamente o recurso.

Como testar bloqueios

Execute testes de carga enquanto coleta event loop lag, utilização, CPU e latência. Inclua payloads grandes, conexões simultâneas e cenários de falha. Um teste com apenas uma requisição não revela contenção.

Também crie testes unitários para funções de transformação e benchmarks separados para caminhos críticos. Isso facilita identificar quando uma alteração aumenta o custo síncrono.

Erros comuns

  • Achar que Node.js nunca bloqueia: JavaScript síncrono ocupa a thread principal.
  • Usar nextTick em recursão: I/O e timers podem sofrer starvation.
  • Confiar na ordem de timeout e immediate: o contexto influencia.
  • Aumentar o thread pool sem medir: a disputa por CPU pode piorar.
  • Processar payload ilimitado: parsing e serialização bloqueiam.
  • Observar apenas CPU média: atrasos curtos podem causar alta latência.
  • Usar métodos Sync em endpoints: todas as conexões aguardam.

Boas práticas para produção

  • Mantenha callbacks pequenos e previsíveis.
  • Evite operações síncronas no caminho de requisições.
  • Divida lotes grandes ou utilize workers.
  • Limite payloads e complexidade.
  • Monitore atraso e utilização do event loop.
  • Respeite backpressure.
  • Use timeouts com cancelamento real.
  • Faça testes de carga representativos.
  • Observe o pool de threads e operações externas.
  • Registre duração dos caminhos críticos.

Conclusão

O Event Loop no Node.js coordena callbacks e permite que uma única thread JavaScript gerencie muitas operações assíncronas. Esse modelo funciona muito bem quando o código síncrono permanece curto e o trabalho pesado é delegado corretamente.

Ao entender fases, microtarefas, nextTick, thread pool e bloqueios, você consegue diagnosticar latência com mais precisão. Monitoramento, limites e divisão de trabalho transformam o event loop de um conceito abstrato em uma ferramenta prática para construir serviços estáveis.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita