O event loop é o mecanismo que faz toda a história de concorrência do Node funcionar: um loop, impulsionado pelo libuv, que decide qual pedaço do seu JavaScript pendente será executado em seguida. É o que permite que uma linguagem de thread único lide com milhares de conexões concorrentes - e o que transforma uma função síncrona descuidada em uma interrupção.
Como o Node.js Funciona cobre a imagem mais ampla de V8 mais libuv; esta página aprofunda um nível no próprio loop - suas fases, suas regras de agendamento e as suposições que o tornam rápido quando respeitado e perigoso quando ignorado. Trate esta página como a âncora conceitual para a seção: os exemplos práticos em Noções Básicas do Event Loop e os aprofundamentos fase a fase e de microtasks que se seguem se baseiam no modelo descrito aqui.
O event loop é um ciclo fixo de fases que o libuv executa repetidamente, cada fase responsável por uma categoria de callback pendente, com a call stack exigida para estar vazia antes que o loop avance.
Por que Importa: Quase toda pergunta de "por que isso rodou antes daquilo?" ou "por que minha API travou?" no Node remonta a como esse ciclo realmente ordena o trabalho - adivinhar geralmente leva à resposta errada.
Conceitos-Chave:call stack, execução até a conclusão (run-to-completion), fase, macrotask, microtask, agendamento cooperativo.
Quando Usar Este Modelo: Para diagnosticar bugs de ordenação, raciocinar sobre latência sob carga, decidir se o trabalho pertence à thread principal e ler as páginas mais detalhadas de fases/microtasks com a moldura correta já definida.
Limitações / Trade-offs: O loop garante a ordem relativa, não o tempo exato - e é cooperativo, o que significa que confia que cada callback terminará rapidamente. Ele não tem como preempir um callback que não o faz.
Tópicos Relacionados: Fases do libuv, microtasks vs. macrotasks, timers e agendamento, detecção de bloqueio do event loop.
O JavaScript no Node segue a execução até a conclusão (run-to-completion): uma vez que uma função começa a executar, ela roda até retornar, e nada mais - nenhum outro callback, nenhuma conclusão de I/O, nada - pode interrompê-la. Todo o trabalho do event loop é decidir o que executa a seguir, e ele só pode tomar essa decisão quando a call stack está completamente vazia.
console.log('a');setTimeout(() => console.log('b'), 0);console.log('c');// a, c, b - "b" não pode rodar até que o código síncrono acima dele termine
Um modelo mental útil: imagine um guarda de segurança noturno fazendo rondas fixas por um prédio. Cada ronda visita as mesmas estações na mesma ordem - verificar os timers na porta da frente, limpar a sala de correspondência, ouvir nos monitores por nova atividade, varrer por algo sinalizado como urgente, trancar o que está fechando - então o guarda recomeça a ronda do topo. O guarda nunca faz duas estações ao mesmo tempo, e nunca pula adiante; se algo urgente acontece entre as rondas, espera o guarda chegar àquela estação. Esse circuito fixo e repetitivo é o event loop - cada "estação" é uma fase, e o guarda é a única thread JavaScript executando o que quer que essa fase lhe entregue.
O loop não é uma infraestrutura opcional ao lado do seu código - é o que mantém o processo Node vivo. Um processo sai quando não há mais nada a fazer: nenhum timer pendente, nenhum socket de servidor aberto, nenhuma operação de I/O enfileirada. Cada server.listen() ou handle aberto é efetivamente uma razão para o guarda continuar fazendo rondas; chame .unref() em um handle para dizer ao loop "não fique vivo apenas por isso".
Cada passagem pelo loop visita as mesmas fases na mesma ordem fixa:
┌───────────────┐
│ timers │ Callbacks de setTimeout / setInterval cujo tempo expirou
├───────────────┤
│ pending cbs │ alguns callbacks de nível de sistema adiados da passagem anterior
├───────────────┤
│ idle, prepare │ apenas para uso interno
├───────────────┤
│ poll │ recupera novos eventos de I/O; executa callbacks de I/O (a maior parte do trabalho acontece aqui)
├───────────────┤
│ check │ callbacks de setImmediate
├───────────────┤
│ close cbs │ ex: socket.on('close', ...)
└───────────────┘
│
└──────────── de volta a timers, para sempre (enquanto houver trabalho)
Esse é o esqueleto - Fases do libuv cobre o que realmente acontece dentro de cada uma delas, incluindo o comportamento de bloqueio da fase poll e como ela decide quando seguir em frente. O que importa neste nível é a forma: é uma sequência fixa, não uma única fila de primeiro a entrar, primeiro a sair. A fase de um callback determina aproximadamente quando ele pode rodar, não a ordem em que você o registrou.
Microtasks - Promises resolvidas, queueMicrotask, process.nextTick - ficam completamente fora desse ciclo de fases. O Node esvazia completamente a fila de microtasks após cada callback, não apenas uma vez por volta completa, então as microtasks sempre têm uma chance de rodar antes que o loop passe de uma fase (ou de um callback) para a próxima. É por isso que um Promise.resolve().then() vence de forma confiável um setTimeout(fn, 0), mesmo que ambos pareçam "rodar em breve" - um é uma macrotask esperando sua fase, o outro entra imediatamente após o callback atual terminar. Microtasks vs Macrotasks cobre as regras exatas de ordenação, incluindo onde process.nextTick se encaixa.
A fase poll merece menção especial porque é onde o loop passa a maior parte do tempo em um aplicativo com I/O intensivo: quando não há mais nada a fazer, o libuv pode deixar o pollbloquear, esperando pelo sistema operacional por novos eventos de I/O, em vez de girar e consumir CPU. Esse é o loop sendo eficiente em ociosidade - ele não está em busy-wait, está dormindo até que o sistema operacional o acorde.
O event loop é um ponto em um espectro de modelos de concorrência, e saber onde ele se encaixa esclarece para o que ele é bom e para o que não é:
Modelo
Força
Fraqueza
Melhor Ajuste
Event loop do Node (cooperativo, thread JS única)
I/O barato e de alta concorrência; modelo mental simples, sem data races em JS
Um callback longo trava tudo que compartilha essa thread
Servidores com I/O intensivo, proxies, gateways em tempo real
Thread OS por requisição (preemptivo)
O SO pode interromper uma requisição lenta; paralelismo real
Custo de memória/agendamento por thread ociosa; condições de corrida para gerenciar
Cargas de trabalho com CPU intensiva ou I/O intensivo por padrão
Green threads / goroutines (ex: Go)
Unidades leves e preemptíveis agendadas pelo runtime
Modelo de memória/GC diferente para raciocinar
Cargas de trabalho mistas de CPU e I/O de alta concorrência
Modelo de ator (ex: Erlang/BEAM)
Isolamento verdadeiro por processo; um ator travado não trava outros
Overhead maior de passagem de mensagens
Sistemas tolerantes a falhas, altamente concorrentes, estilo telecomunicações
A escolha do Node - agendamento cooperativo em uma única thread - é por que a plataforma é excepcionalmente boa em I/O de alta concorrência e excepcionalmente ruim em absorver silenciosamente um erro de CPU. Não há rede de segurança em nível de SO; o loop confia que cada callback devolverá o controle rapidamente. É também por isso que autores de frameworks (Express, Fastify, NestJS) constroem roteamento e middleware com a suposição de que os manipuladores retornam o controle prontamente - um middleware bloqueador não apenas atrasa sua própria requisição, mas atrasa toda a ronda do guarda.
Em produção, essa natureza cooperativa é exatamente o que você deve observar: atraso ou utilização crescente do event loop (veja Detectando Bloqueio do Event Loop) significa que algum callback, em algum lugar, não está cooperando. Melhores Práticas do Event Loop transforma este modelo em regras operacionais concretas - o que manter fora da thread principal e como estruturar manipuladores para que o loop permaneça livre.
"O event loop é uma única fila, e os callbacks rodam na ordem em que foram registrados." É uma sequência fixa de fases, cada uma com sua própria fila - o tipo de um callback (timer, I/O, setImmediate) determina qual fase o manipula, não apenas a ordem de registro.
"Promises e setTimeout são agendados da mesma forma, apenas com atrasos diferentes." São categorias completamente diferentes - Promises são microtasks que são esvaziadas entre cada callback; callbacks de setTimeout são macrotasks ligadas a uma fase específica. Elas não são comparáveis por "atraso".
"O loop pesquisa constantemente em um busy-wait, consumindo CPU mesmo quando ocioso." A fase poll pode bloquear e deixar o sistema operacional acordá-lo quando o I/O estiver pronto - um processo Node ocioso não está girando um núcleo de CPU.
"Se eu usar async/await, o event loop não pode travar no meu código."async muda como um resultado é entregue, não se as partes síncronas dessa função podem ocupar a call stack. Um loop longo dentro de uma função async bloqueia exatamente tanto quanto faria fora de uma.
"O event loop garante que meu callback rode em um horário específico." Ele garante regras de ordenação relativas (microtasks antes da próxima fase, fase de timers antes de poll, etc.), nunca um timestamp exato - setTimeout(fn, 0) significa "não antes de", não "exatamente às".
Um ciclo repetitivo de fases fixas, executado pelo libuv, que decide qual callback pendente recebe a única thread JavaScript a seguir - e só avança quando a call stack do callback atual está vazia.
Por que o Node usa um event loop em vez de uma thread por requisição?
Porque a maior parte do trabalho do servidor é gasta esperando por I/O, não computando - uma única thread que nunca bloqueia em I/O pode atender a muito mais conexões concorrentes do que uma thread por conexão, a maioria das quais ficaria ociosa esperando. Veja Como o Node.js Funciona para o raciocínio completo de V8/libuv.
Como o event loop se relaciona com a call stack?
O loop só pode escolher o próximo callback quando a call stack está completamente vazia - a regra de execução até a conclusão do JavaScript significa que nada preempte uma função em execução, então a parte "loop" é realmente apenas "o que acontece entre uma pilha vazia e a próxima".
As microtasks fazem parte do ciclo de fases?
Não - as microtasks (Promises, queueMicrotask, process.nextTick) são esvaziadas completamente após cada callback, independentemente da fase que acabou de rodar. Elas têm prioridade sobre passar para a próxima fase ou a próxima macrotask.
Por que um `setTimeout(fn, 0)` roda depois de um `Promise.resolve().then()`?
O callback da Promise é uma microtask e é esvaziado imediatamente após o código em execução terminar. O callback do timer é uma macrotask que tem que esperar o loop chegar à fase de timers em uma passagem posterior - as microtasks sempre vencem essa corrida.
O que realmente mantém um processo Node vivo?
Qualquer timer pendente, handle aberto (como um servidor escutando ou socket), ou operação de I/O enfileirada. Assim que nenhum desses permanecer, o loop não terá mais nada para ciclar e o processo sairá naturalmente - .unref() em um handle o exclui de contar para "manter o processo vivo".
O event loop faz busy-wait quando não há nada a fazer?
Não. A fase poll pode bloquear, permitindo que o sistema operacional acorde o processo quando o I/O estiver realmente pronto, em vez de girar e consumir CPU enquanto ocioso.
Por que uma requisição lenta afeta requisições que não têm nada a ver com ela?
Todas compartilham a mesma call stack. Uma seção síncrona e intensiva em CPU de qualquer callback ocupa completamente a única thread JS, então o loop não pode avançar para nenhum outro callback pendente - incluindo os de requisições de outra forma não relacionadas - até que retorne.
O event loop do navegador é o mesmo do Node?
Eles compartilham a mesma ideia central - executar até a conclusão, esvaziar microtasks, então lidar com a próxima tarefa - mas os detalhes diferem. Navegadores intercalam renderização e outras tarefas específicas de UI em seu loop; o loop do Node é construído em torno do modelo de fases do libuv (timers, poll, check, etc.) sem nenhuma etapa de renderização.
O event loop pode ser preemptado, da mesma forma que um SO preempte threads?
Não - é cooperativo. Nada força um callback de longa execução a ceder o controle de volta para o loop; ele tem que retornar por conta própria (ou passar o trabalho para worker_threads/o pool de threads do libuv) para que qualquer outra coisa possa rodar.
Qual é a diferença entre uma "fase" e uma "fila de microtasks"?
Uma fase é uma parada no circuito fixo do loop (timers, poll, check, …), cada uma lidando com uma categoria de macrotask. A fila de microtasks não é uma fase - ela é esvaziada por completo após cada callback individual, independente da fase, razão pela qual as microtasks consistentemente rodam antes do trabalho da próxima fase.
Múltiplos processos Node compartilham um único event loop?
Não - cada processo Node tem seu próprio event loop independente e sua própria thread JS única. Ferramentas como o módulo cluster ou um gerenciador de processos executam múltiplos processos separados, cada um com seu próprio loop, para usar mais de um núcleo de CPU.