Voltar para todos os posts
·7 min de leitura

Event Loop no Node.js: o DJ que nunca para de tocar

O Node.js roda em uma única thread, mas dá conta de milhares de operações ao mesmo tempo. Veja como o Event Loop, a Call Stack e as filas de tarefas trabalham juntos, com a analogia de um DJ numa festa.

Node.jsJavaScriptEvent LoopAsyncIniciantes

Event Loop no Node.js: o DJ que nunca para de tocar

Node.js é single-threaded. Ou seja, ele só faz uma coisa por vez. Mesmo assim, aguenta milhares de conexões simultâneas sem suar. Como?

A resposta é o Event Loop, e entender como ele funciona muda o jeito como você pensa em código assíncrono.

Para explicar, vou usar a analogia de um DJ numa festa.

A mesa do DJ (Call Stack)

Imagine um DJ numa festa. A mesa do DJ (a Call Stack) só consegue tocar uma música por vez. Não importa quantos pedidos cheguem: o DJ atende um por um, em ordem.

Em termos de JavaScript, essa é a call stack, uma estrutura LIFO (Last In, First Out) onde funções são empilhadas quando chamadas e removidas quando retornam.

function receberConvidado(nome) {
  console.log(`Bem-vindo, ${nome}!`);
}

function abrirFesta() {
  receberConvidado("Alice");
  receberConvidado("Bob");
}

abrirFesta();
// Saída:
// Bem-vindo, Alice!
// Bem-vindo, Bob!

Cada chamada de função é empilhada, executada e removida antes de a próxima rodar. É simples, sequencial e previsível.

Os roadies (internals do Node / libuv)

O DJ não pode sair da mesa para configurar a máquina de fumaça. Esse é o trabalho dos roadies.

No Node.js, os roadies são os internals do Node: a libuv e a camada assíncrona do sistema operacional, o equivalente ao que os navegadores chamam de Web APIs. É por ali que passam setTimeout, fetch, leitura de arquivos e requisições de rede. Quando você chama uma dessas funções, o Node entrega a tarefa para o ambiente e segue em frente na hora. O DJ continua tocando enquanto os roadies fazem o trabalho deles.

console.log("A festa começa");

setTimeout(() => {
  console.log("Máquina de fumaça pronta!");
}, 2000);

console.log("O DJ já está tocando");

// Saída:
// A festa começa
// O DJ já está tocando
// Máquina de fumaça pronta! (2 segundos depois)

O Node não ficou parado esperando 2 segundos. Passou a tarefa para o roadie e seguiu em frente. Isso é I/O não bloqueante.

A fila de pedidos (Macrotask Queue)

Quando um roadie termina o trabalho, o resultado não vai direto para a mesa do DJ. Ele vai para a fila de pedidos, a Macrotask Queue.

O Event Loop (a disciplina do DJ) tem uma regra: só pegar o próximo pedido quando a mesa estiver completamente livre.

console.log("1 - DJ abre o set");

setTimeout(() => console.log("3 - Primeiro pedido da fila"), 0);
setTimeout(() => console.log("4 - Segundo pedido da fila"), 0);

console.log("2 - DJ termina a abertura");

// Saída:
// 1 - DJ abre o set
// 2 - DJ termina a abertura
// 3 - Primeiro pedido da fila
// 4 - Segundo pedido da fila

Mesmo com setTimeout(..., 0), os callbacks passam pela fila e só rodam depois que a call stack atual esvazia.

Exemplos de macrotasks: setTimeout, setInterval, callbacks de I/O, setImmediate (Node.js).

A fila VIP (Microtask Queue)

Alguns convidados da festa são VIPs. Eles têm uma fila separada, e o DJ segue uma política rígida: depois de cada música (macrotask), ele esvazia toda a fila VIP antes de tocar a próxima música da fila normal. A fila VIP também é atendida assim que a call stack inicial esvazia, antes mesmo de tocar a primeira música da fila normal.

A Microtask Queue funciona assim: ela é esvaziada por completo depois de cada macrotask, antes de a próxima macrotask começar.

console.log("1 - Início");

setTimeout(() => console.log("5 - Macrotask (setTimeout)"), 0);

Promise.resolve()
  .then(() => console.log("3 - Microtask 1 (Promise)"))
  .then(() => console.log("4 - Microtask 2 (Promise encadeada)"));

console.log("2 - Fim do código síncrono");

// Saída:
// 1 - Início
// 2 - Fim do código síncrono
// 3 - Microtask 1 (Promise)
// 4 - Microtask 2 (Promise encadeada)
// 5 - Macrotask (setTimeout)

Os callbacks das Promises (microtasks) sempre rodam antes do callback do setTimeout (macrotask), mesmo que o setTimeout tenha sido registrado primeiro.

Exemplos de microtasks: Promise.then, Promise.catch, Promise.finally, queueMicrotask, continuações de async/await.

O set completo

Juntando tudo num exemplo só:

console.log("1 - Síncrono: a festa começa");

setTimeout(() => console.log("6 - Macrotask: trabalho do roadie concluído"), 0);

Promise.resolve()
  .then(() => {
    console.log("4 - Microtask: convidado VIP chega");
  })
  .then(() => console.log("5 - Microtask: VIP recebe sua bebida"));

setTimeout(() => console.log("7 - Macrotask: segundo roadie concluído"), 0);

console.log("2 - Síncrono: DJ continua tocando");
console.log("3 - Síncrono: call stack esvazia");

// Saída:
// 1 - Síncrono: a festa começa
// 2 - Síncrono: DJ continua tocando
// 3 - Síncrono: call stack esvazia
// 4 - Microtask: convidado VIP chega
// 5 - Microtask: VIP recebe sua bebida
// 6 - Macrotask: trabalho do roadie concluído
// 7 - Macrotask: segundo roadie concluído

Ordem de execução:

  1. Todo o código síncrono (call stack esvazia completamente)
  2. Todas as microtasks (a fila VIP é esvaziada por completo)
  3. Uma macrotask (fila normal)
  4. Todas as microtasks de novo (a fila VIP é esvaziada outra vez)
  5. Próxima macrotask... e assim por diante

3 regras de ouro

  1. Uma coisa por vez. A call stack roda em uma única thread, sem paralelismo. Ela dá conta de muitas tarefas porque delega, e não porque faz tudo ao mesmo tempo.
  2. Microtasks sempre vêm antes das macrotasks. Depois de cada macrotask, a fila de microtasks inteira é esvaziada primeiro.
  3. Operações assíncronas não bloqueiam. Elas são delegadas ao ambiente (os roadies), e a call stack fica livre para continuar trabalhando.

Essas três regras explicam 90% do comportamento "estranho" que você vai encontrar em JavaScript assíncrono no Node.js.