Event loop do Node: por que JS não trava sem thread
Entenda como o Node processa requisições sem travar mesmo sendo single-threaded, a diferença entre microtask e macrotask, e a pegadinha de um loop síncrono pesado bloqueando o processamento de todo o resto.
Já falei aqui sobre fila de mensagem, condição de corrida e connection pooling, assuntos que mexem com concorrência de um jeito ou de outro. Mas ficou faltando a base que explica como o próprio Node lida com várias coisas "ao mesmo tempo" tendo, na essência, uma única thread rodando o código JavaScript. É o event loop, e entender ele resolve bastante confusão sobre o que trava e o que não trava.
Single-threaded não significa sequencial e lento
JavaScript no Node roda em uma única thread, o que à primeira vista parece um problema: como um servidor atende milhares de requisições simultâneas com só uma linha de execução? A resposta está em como operações de entrada e saída (ler arquivo, consultar banco, fazer requisição de rede) são tratadas: elas não bloqueiam essa thread única esperando a resposta, são delegadas pro sistema operacional (ou pra uma thread interna do próprio Node, fora da sua visão), e o JavaScript segue livre pra processar outra coisa enquanto isso.
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");O resultado impresso é 1, 3, 2, nessa ordem, mesmo o setTimeout tendo um delay de zero milissegundos. Isso acontece porque o setTimeout, por mais que o tempo configurado seja mínimo, sempre agenda a execução pra depois do código síncrono atual terminar, nunca no meio dele.
A fila de callback e o loop
O event loop é, na essência, um ciclo que fica checando se existe algo pronto pra rodar: uma resposta de rede que já chegou, um timer que já venceu, uma leitura de arquivo que já terminou. Enquanto o código síncrono está rodando, nada dessas tarefas pendentes é processado, elas ficam esperando na fila até a thread principal ficar livre.
function tarefaPesada() {
const inicio = Date.now();
while (Date.now() - inicio < 3000) {} // trava por 3 segundos
}
setTimeout(() => console.log("isso deveria rodar rápido"), 100);
tarefaPesada();Mesmo configurando o setTimeout pra 100 milissegundos, o console.log só aparece depois dos 3 segundos inteiros de tarefaPesada, porque o loop não consegue processar nada da fila enquanto a thread principal está presa num laço síncrono. Esse é o ponto mais importante de entender: operação assíncrona não trava a thread, mas operação síncrona pesada trava tudo, inclusive toda a parte assíncrona que estava esperando pra rodar.
Microtask x macrotask
Dentro da fila de tarefas pendentes, existe uma ordem de prioridade. Microtasks (como o que vem de uma Promise resolvida) são processadas antes de macrotasks (como setTimeout ou setInterval), mesmo que a macrotask tenha sido agendada primeiro:
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("síncrono");A saída vem na ordem síncrono, microtask, macrotask. Primeiro todo o código síncrono termina, depois o loop esvazia completamente a fila de microtask antes de pegar a próxima macrotask da fila. Isso explica um comportamento que confunde bastante gente: encadear várias Promise em sequência pode, em teoria, atrasar indefinidamente um setTimeout que já estava esperando, porque a fila de microtask sempre tem prioridade sobre a de macrotask.
Onde isso aparece de verdade no backend
Entender isso importa na prática porque qualquer trecho de código síncrono e pesado dentro de uma rota (um laço gigante processando dado em memória, um cálculo matemático complexo) bloqueia o processamento de toda outra requisição que chegue nesse meio tempo, mesmo que ela não tenha nada a ver com a que está travando. Um servidor Node não trava numa requisição esperando banco de dados responder, mas trava completamente numa requisição que faz conta pesada em JavaScript puro, sem nenhuma chamada assíncrona no meio.
A saída pra isso, quando o processamento pesado é inevitável, é mover esse trabalho pra fora da thread principal, usando um worker thread, ou delegando pra uma fila de mensagem, que já expliquei aqui, processada por um serviço separado em vez de travar o processo que está atendendo requisição.
Fechando
O event loop não é sobre o Node fazer várias coisas ao mesmo tempo de verdade, é sobre nunca ficar esperando parado numa operação de entrada e saída, aproveitando esse tempo de espera pra processar outra coisa. O que ele não resolve é código síncrono pesado, que ocupa a única thread disponível e trava literalmente tudo o mais até terminar. Saber diferenciar os dois é o que evita a surpresa de um servidor que parece rápido o tempo todo, até o dia em que uma rota com processamento pesado deixa todas as outras penduradas ao mesmo tempo.
Direto na sua
caixa de entrada.
Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.
Leia também
- Claude Design: os 5 níveis de uso, do básico ao pro19 de outubro de 2026 · 6 min
- Idempotency key: evite cobrar duas vezes no retry15 de outubro de 2026 · 4 min
- CSRF: o ataque que usa sua sessão contra você14 de outubro de 2026 · 4 min
- Bolio UI: componentes React que a IA realmente entende12 de outubro de 2026 · 4 min