Se você já começou a estudar Node.js, provavelmente esbarrou no termo Event Loop e talvez tenha pensado: “ok, isso parece importante, mas o que exatamente é isso?”. E está tudo bem, porque essa é uma das partes mais confusas para quem está começando, mas ao mesmo tempo é uma das mais importantes.
O Event Loop é basicamente o que permite que o Node.js execute várias tarefas ao mesmo tempo, mesmo usando apenas uma thread. Parece meio contraditório no começo, mas é exatamente isso que torna o Node tão eficiente, principalmente para aplicações que lidam com muitas requisições, como APIs e sistemas em tempo real.
Diferente de outras linguagens que criam várias threads para lidar com múltiplas tarefas, o Node segue um modelo mais leve, baseado em eventos e callbacks. E é aí que entra o Event Loop, funcionando como um gerenciador que decide o que deve ser executado e em qual momento.
Neste artigo, eu vou te explicar isso de forma bem direta, sem complicação e sem termos desnecessários. A ideia é que você realmente entenda o conceito e consiga aplicar no seu dia a dia como desenvolvedor. Mesmo que hoje isso pareça abstrato, no final você vai enxergar com mais clareza como tudo funciona por baixo dos panos.
O que é o Event Loop no Node.js
Agora que você já entendeu o contexto, vamos direto ao ponto: o que exatamente é o Event Loop?
Pensa no Event Loop como um organizador de tarefas. Ele não executa o código diretamente, mas decide quando cada pedaço de código deve rodar. É como se fosse um gerente olhando uma fila de tarefas e dizendo: “ok, agora é a vez dessa aqui”.
No Node.js, tudo gira em torno disso. Como ele trabalha com apenas uma thread principal, ele precisa de uma forma eficiente de não travar enquanto espera coisas mais lentas acontecerem, como uma leitura de banco de dados ou uma chamada de API. E é aí que o Event Loop entra.
Quando você faz uma operação assíncrona, como um setTimeout ou uma requisição HTTP, o Node não fica esperando aquilo terminar. Em vez disso, ele registra essa tarefa em outro lugar e continua executando o restante do código normalmente. Quando essa operação termina, ela vai para uma fila de espera.
O Event Loop fica o tempo todo verificando duas coisas: se a pilha de execução está vazia e se existe alguma tarefa esperando na fila. Se estiver tudo livre, ele pega a próxima tarefa da fila e manda executar.
Isso acontece o tempo inteiro, em ciclos muito rápidos. Por isso o nome “loop”. É literalmente um loop infinito verificando o que precisa ser feito.
O mais interessante é que, graças a esse modelo, o Node consegue lidar com milhares de conexões ao mesmo tempo sem precisar criar várias threads. E é exatamente isso que faz dele uma escolha tão popular para aplicações modernas.
Como o Node.js executa código
Antes de aprofundar mais no Event Loop, você precisa entender como o Node.js executa o código no dia a dia. Sem isso, tudo vira meio abstrato.
A primeira coisa importante é: o Node.js é single thread. Isso significa que ele executa uma coisa por vez, em uma única linha de execução. Diferente de outras plataformas que criam várias threads para lidar com múltiplas tarefas ao mesmo tempo, o Node segue um caminho mais simples e eficiente.
Mas aí vem a dúvida natural: como ele consegue lidar com várias coisas ao mesmo tempo?
A resposta está no modelo assíncrono.
Quando você escreve código síncrono, o Node executa linha por linha, esperando cada instrução terminar antes de ir para a próxima. Por exemplo:
console.log("A");
console.log("B");
console.log("C");
Aqui não tem mistério. Vai imprimir A, depois B, depois C.
Agora olha um exemplo com algo assíncrono:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
Muita gente espera que o B venha logo depois do A, mas na prática o resultado é:
A
C
B
Isso acontece porque o setTimeout não é executado imediatamente. Ele é delegado para outro mecanismo do Node e só volta para execução depois.
E é exatamente nesse ponto que entram três peças fundamentais:
- Call Stack: onde o código é executado
- APIs do Node: onde tarefas assíncronas são processadas
- Filas de callbacks: onde tarefas aguardam para serem executadas
O Node executa tudo que é síncrono primeiro, jogando funções na Call Stack. Quando aparece algo assíncrono, ele não trava o fluxo. Ele delega essa tarefa e segue em frente.
Mais pra frente, quando essa tarefa estiver pronta, ela entra em uma fila. E aí o Event Loop decide quando ela pode ser executada.
Pode parecer simples, mas esse modelo é o que permite que aplicações Node sejam rápidas e escaláveis.
Entendendo o Call Stack na prática
Agora vamos olhar para a base de tudo: a Call Stack. Se você entender bem essa parte, o resto começa a fazer muito mais sentido.
A Call Stack, ou pilha de execução, é onde o Node.js coloca as funções para executar. E ela funciona exatamente como uma pilha mesmo. O último item que entra é o primeiro que sai.
Vamos ver um exemplo simples:
function primeira() {
console.log("Primeira");
}
function segunda() {
console.log("Segunda");
}
primeira();
segunda();
Aqui o fluxo é direto. O Node pega a função primeira, coloca na pilha, executa e remove. Depois faz o mesmo com segunda. Nada de especial.
Agora vamos complicar um pouco:
function um() {
dois();
}
function dois() {
tres();
}
function tres() {
console.log("Executando...");
}
um();
O que acontece aqui por baixo dos panos é o seguinte:
- A função um entra na Call Stack
- Dentro dela, dois é chamada e entra na pilha
- Dentro de dois, tres é chamada e entra na pilha
- tres é executada
- Depois ela sai da pilha
- dois termina e sai
- um termina e sai
A pilha vai crescendo conforme funções chamam outras funções e vai esvaziando quando elas terminam.
Agora vem o ponto importante: enquanto a Call Stack não estiver vazia, nada de assíncrono entra para execução.
Isso significa que, se você tiver um código pesado ou um loop muito grande bloqueando a pilha, o Node simplesmente não consegue processar outras tarefas, mesmo que elas já estejam prontas.
Por exemplo:
setTimeout(() => {
console.log("Timeout");
}, 0);
for (let i = 0; i < 1e9; i++) {}
console.log("Fim");
Mesmo com o tempo zero, o “Timeout” só vai aparecer depois que o loop terminar. Isso acontece porque a Call Stack ainda está ocupada.
Esse é um dos erros mais comuns de quem está começando: achar que assíncrono significa “executa na hora certa automaticamente”. Não. Ele depende da pilha estar livre.
Guarda bem isso: o Event Loop só consegue agir quando a Call Stack está vazia.
O que é a Callback Queue
Agora que você já entendeu a Call Stack, fica mais fácil visualizar o próximo passo: para onde vão as tarefas assíncronas enquanto esperam para serem executadas?
É aí que entra a Callback Queue, ou fila de callbacks.
Pensa nela como uma fila de atendimento. Quando uma operação assíncrona termina, ela não vai direto para execução. Ela entra nessa fila e espera a sua vez.
Vamos usar um exemplo simples:
console.log("Início");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("Fim");
Aqui está o que acontece por trás:
- O console.log(“Início”) entra na Call Stack e executa
- O setTimeout é encontrado, mas não vai direto para a pilha
- Ele é enviado para as APIs do Node (ou do ambiente)
- O console.log(“Fim”) executa normalmente
- Quando o tempo do setTimeout termina, o callback dele vai para a Callback Queue
Agora vem o ponto chave: mesmo estando pronta, essa função ainda não executa imediatamente.
Ela fica esperando até que a Call Stack esteja vazia.
Só depois disso o Event Loop entra em ação, pega essa função da fila e coloca na Call Stack para execução.
Por isso o resultado final é:
Início
Fim
Timeout
Esse comportamento é o que garante que o código síncrono tenha prioridade. O Node sempre termina tudo que está na pilha antes de começar a puxar coisas da fila.
Outro ponto importante: podem existir várias filas dependendo do tipo de tarefa. Mas por enquanto, pensa em uma fila principal de callbacks que aguardam execução.
Esse conceito é simples, mas extremamente importante. Se você entender bem isso, já está na frente de muita gente que usa Node no dia a dia sem saber exatamente o que está acontecendo.
Como o Event Loop realmente funciona
Agora sim, chegou a parte mais importante do artigo. Aqui é onde tudo se conecta: Call Stack, APIs, filas e o próprio Event Loop.
Até agora você viu as peças separadas. Agora vamos montar o quebra-cabeça.
O Event Loop é basicamente um processo que roda o tempo todo, verificando se pode executar novas tarefas. Ele faz sempre a mesma pergunta: a Call Stack está vazia?
Se a resposta for não, ele espera.
Se for sim, ele olha para as filas e pega a próxima tarefa disponível.
Simples assim.
Mas vamos colocar isso em um fluxo real para ficar mais claro:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
Passo a passo do que acontece:
- “A” entra na Call Stack e executa
- setTimeout é encontrado e enviado para as APIs do ambiente
- “C” entra na Call Stack e executa
- A Call Stack fica vazia
- O callback do setTimeout vai para a Callback Queue
- O Event Loop verifica que a pilha está livre
- Ele pega “B” da fila e coloca na Call Stack
- “B” é executado
Resultado final:
A
C
B
O ponto mais importante aqui é: o Event Loop não executa nada diretamente. Ele só coordena.
Ele garante que:
- Nada interrompa o que já está sendo executado
- Tarefas assíncronas só rodem quando for seguro
- A ordem de execução seja previsível dentro desse modelo
Agora um detalhe que muita gente ignora: o Event Loop não trabalha só com uma fila simples. Existem prioridades diferentes, e algumas tarefas passam na frente de outras.
Por exemplo, Promises (que você vai ver no próximo tópico) têm prioridade maior do que setTimeout.
Isso significa que nem toda tarefa assíncrona é tratada da mesma forma.
Mas segura isso por enquanto.
Se você entendeu até aqui, você já tem uma visão muito mais clara do que a maioria dos devs iniciantes. E isso vai impactar diretamente na forma como você escreve código, principalmente quando começar a lidar com performance e concorrência.
Microtasks vs Macrotasks
Agora a gente entra em um nível que separa quem só usa Node de quem realmente entende o que está acontecendo.
Até aqui você viu que existe uma fila de callbacks. Mas na prática não é uma fila única. Existem tipos diferentes de filas, com prioridades diferentes. E é isso que explica vários comportamentos que parecem estranhos no começo.
As duas principais categorias são: macrotasks e microtasks.
Macrotasks são tarefas mais “comuns”, como:
- setTimeout
- setInterval
- setImmediate
Microtasks são tarefas com prioridade maior, como:
- Promises (.then, .catch, .finally)
- queueMicrotask
A diferença prática é simples: microtasks sempre são executadas antes das macrotasks, assim que a Call Stack fica vazia.
Vamos ver isso com um exemplo:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
Promise.resolve().then(() => {
console.log("C");
});
console.log("D");
Muita gente espera algo como A B C D ou algo nessa linha. Mas o resultado real é:
A
D
C
B
Agora entende o porquê:
- “A” executa
- setTimeout é enviado para as APIs
- Promise.resolve é resolvida e seu then vai para a fila de microtasks
- “D” executa
- A Call Stack fica vazia
- O Event Loop verifica as microtasks primeiro
- “C” executa
- Depois ele vai para as macrotasks
- “B” executa
Ou seja, sempre que o Node termina um ciclo da Call Stack, ele esvazia todas as microtasks antes de olhar para as macrotasks.
Esse detalhe é extremamente importante quando você começa a usar async e await, porque por baixo dos panos eles usam Promises, ou seja, microtasks.
Se você não entende isso, pode acabar criando bugs difíceis de perceber, principalmente relacionados à ordem de execução.
Resumo direto:
- Código síncrono sempre primeiro
- Depois microtasks
- Depois macrotasks
Entender isso muda completamente a forma como você lê e escreve código assíncrono.
Exemplo prático passo a passo
Agora vamos juntar tudo em um exemplo mais completo e analisar com calma. Esse é o tipo de exercício que realmente fixa o entendimento.
Olha esse código:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
Promise.resolve().then(() => {
console.log("4");
});
console.log("5");
Antes de continuar, tenta pensar qual seria a ordem de execução.
Se você ainda fica em dúvida, é totalmente normal. Vamos quebrar isso passo a passo.
Primeiro, o Node começa executando tudo que é síncrono:
- console.log(“1”) executa
- setTimeout é enviado para as APIs
- primeira Promise é resolvida e seu then vai para a fila de microtasks
- segunda Promise também vai para microtasks
- console.log(“5”) executa
Até aqui, a saída é:
1
5
Agora a Call Stack está vazia. O Event Loop entra em ação.
Ele sempre verifica primeiro as microtasks:
- executa o console.log(“3”)
- executa o console.log(“4”)
Agora sim ele vai para as macrotasks:
- executa o console.log(“2”)
Resultado final:
1
5
3
4
2
Percebe o padrão?
- Tudo síncrono primeiro
- Depois todas as microtasks
- Depois as macrotasks
Agora um detalhe importante: as microtasks são executadas até esvaziar completamente a fila. Ou seja, se dentro de uma microtask você criar outra, ela também será executada antes de ir para macrotasks.
Exemplo:
Promise.resolve().then(() => {
console.log("A");
Promise.resolve().then(() => {
console.log("B");
});
});
setTimeout(() => {
console.log("C");
}, 0);
Resultado:
A
B
C
Isso acontece porque o Node continua processando microtasks até não ter mais nenhuma.
Esse tipo de comportamento pode parecer detalhe, mas faz muita diferença em sistemas reais, principalmente quando você começa a trabalhar com múltiplas Promises, filas e processamento assíncrono mais complexo.
Se você chegou até aqui entendendo esse fluxo, você já tem uma base muito sólida de como o Node realmente funciona por dentro.
Por que o Event Loop torna o Node.js rápido
Agora que você já entendeu como tudo funciona por dentro, fica mais fácil responder uma pergunta importante: por que o Node.js é considerado tão rápido?
A resposta não está em “executar mais rápido”, mas em “não perder tempo esperando”.
Em muitas aplicações, principalmente APIs, a maior parte do tempo não é gasto processando dados, mas esperando respostas. Pode ser uma consulta no banco, uma chamada para outra API, leitura de arquivo, coisas assim.
Em modelos tradicionais, cada requisição pode ocupar uma thread inteira, mesmo enquanto está esperando. Isso consome memória e limita a escalabilidade.
O Node.js resolve isso de forma diferente.
Quando uma operação de IO acontece, o Node não fica parado esperando. Ele delega essa tarefa para o sistema e continua atendendo outras requisições. Quando a resposta estiver pronta, ela entra na fila e será processada no momento certo.
Isso significa que, com poucos recursos, o Node consegue lidar com muitas conexões ao mesmo tempo.
Vamos pensar em um cenário simples:
- 1000 usuários acessando uma API
- Cada requisição faz uma chamada ao banco
- O banco leva 100ms para responder
Em um modelo bloqueante, cada requisição poderia ficar parada esperando esses 100ms.
No Node, essas 1000 requisições podem ser iniciadas quase ao mesmo tempo. Enquanto o banco responde, o Event Loop continua trabalhando com outras tarefas.
Outro ponto importante é que não existe o custo de criar e gerenciar múltiplas threads o tempo todo. Isso deixa a aplicação mais leve.
Mas aqui vai um alerta importante: isso não significa que o Node é sempre a melhor escolha.
Se você colocar uma tarefa pesada de CPU, como um cálculo complexo ou processamento de imagem diretamente na Call Stack, você bloqueia o Event Loop. E aí tudo para.
Por isso o Node é excelente para tarefas de IO, mas precisa de cuidado com processamento pesado.
Resumo direto:
- Node não bloqueia esperando respostas
- Usa bem poucos recursos
- Consegue escalar com facilidade
- Mas pode travar se você usar errado
Erros comuns de iniciantes com Event Loop
Aqui é onde muita gente se complica. Não porque o conceito é impossível, mas porque alguns detalhes passam despercebidos no dia a dia.
Se você evitar esses erros, já sai na frente de muita gente.
O primeiro erro é achar que Node.js é multithread por padrão.
Muita gente pensa que, por conseguir lidar com várias requisições ao mesmo tempo, o Node está executando várias coisas em paralelo dentro da mesma aplicação. Não é bem assim. Existe apenas uma thread principal rodando o JavaScript. O “paralelismo” vem do modelo assíncrono e da delegação de tarefas.
O segundo erro é bloquear a Call Stack sem perceber.
Um loop muito grande, um processamento pesado ou até um código mal otimizado pode travar completamente o Event Loop. E quando isso acontece, nada mais roda. Nem requisições, nem callbacks, nada.
Exemplo clássico:
app.get("/rota", (req, res) => {
for (let i = 0; i < 1e9; i++) {}
res.send("ok");
});
Enquanto esse loop roda, o servidor inteiro fica travado.
O terceiro erro é não entender a ordem de execução de Promises.
Muita gente acha que setTimeout com 0 executa “imediatamente”, ou que async e await funcionam como código síncrono puro. Isso gera bugs sutis, principalmente quando você depende da ordem de execução.
Outro erro comum é exagerar no uso de código assíncrono sem necessidade.
Nem tudo precisa ser async. Usar Promises para tudo pode deixar o código mais difícil de ler e até mais difícil de debugar.
E por último, ignorar o impacto das microtasks.
Como você viu, microtasks têm prioridade. Se você criar muitas delas em sequência, pode atrasar a execução de outras tarefas importantes.
Resumo rápido dos erros:
- Pensar que Node executa tudo em paralelo
- Bloquear o Event Loop com código pesado
- Não entender a ordem entre microtasks e macrotasks
- Usar async sem necessidade
- Ignorar como filas realmente funcionam
Esses pontos parecem simples, mas são exatamente os que mais causam problemas em produção.
Boas práticas ao trabalhar com o Event Loop
Agora que você já entendeu como o Event Loop funciona e quais são os erros mais comuns, vamos para a parte prática: como usar isso a seu favor no dia a dia.
A primeira prática, e talvez a mais importante, é evitar bloquear a Call Stack.
Sempre que você tiver uma tarefa pesada de CPU, como processamento de dados grandes, criptografia ou loops intensos, tente não executar isso diretamente na thread principal. Em vez disso, você pode usar estratégias como filas, workers ou até serviços separados.
A segunda é usar async e await com consciência.
Eles deixam o código mais legível, mas não mudam o funcionamento por baixo dos panos. Ainda são Promises, ainda entram como microtasks. Então é importante lembrar que a ordem de execução continua seguindo as mesmas regras que você aprendeu.
Outra prática importante é lidar bem com operações de IO.
Node é excelente nisso, então aproveite. Use chamadas assíncronas para banco de dados, APIs externas e arquivos. Evite versões síncronas dessas operações, principalmente em aplicações web.
Também vale prestar atenção em loops com operações assíncronas.
Um erro comum é usar forEach com async, o que pode gerar comportamentos inesperados. Em muitos casos, usar for…of com await ou Promise.all é mais previsível.
Exemplo melhor:
const resultados = await Promise.all(
itens.map(item => processar(item))
);
Assim você garante controle sobre execução e desempenho.
Outra boa prática é monitorar sua aplicação.
Ferramentas de observabilidade ajudam a identificar quando o Event Loop está sendo bloqueado ou quando existem gargalos de performance.
E por fim, mantenha o código simples.
Quanto mais complexo o fluxo assíncrono, maior a chance de erro. Se você conseguir escrever algo de forma clara, já está no caminho certo.
Resumo direto:
- Evite tarefas pesadas na thread principal
- Use async e await com entendimento, não só por hábito
- Prefira operações assíncronas de IO
- Tenha cuidado com loops assíncronos
- Monitore performance sempre que possível
Se você aplicar essas práticas, o Event Loop deixa de ser um problema e passa a ser uma das maiores vantagens do Node.js.
Conclusão
Se você chegou até aqui, já tem uma base muito sólida sobre como o Event Loop funciona no Node.js. E mais importante do que decorar termos como Call Stack, microtasks ou macrotasks, é entender o comportamento por trás de tudo isso.
No começo pode parecer confuso, principalmente porque muita coisa acontece “nos bastidores”. Mas quando você entende que o Node segue um fluxo simples, executa o que é síncrono, depois microtasks, depois macrotasks, tudo começa a fazer mais sentido.
O Event Loop não é algo mágico. Ele é só um mecanismo muito bem pensado para organizar a execução do código de forma eficiente. E é exatamente isso que permite que o Node lide com muitas requisições ao mesmo tempo sem precisar de uma estrutura pesada.
Esse conhecimento muda a forma como você escreve código. Você começa a evitar bloqueios, entende melhor a ordem de execução e consegue prever comportamentos que antes pareciam aleatórios.
E isso faz diferença de verdade. Principalmente quando você começa a trabalhar em aplicações maiores, onde performance e concorrência importam.
Agora é prática. Testa os exemplos, cria variações, tenta prever a saída antes de rodar o código. Esse tipo de treino acelera muito o aprendizado.
Se esse conteúdo te ajudou de alguma forma, comenta aqui embaixo. Quero saber se você já tinha ouvido falar do Event Loop e qual parte ainda parece mais difícil para você.