Node.js é JavaScript rodando fora de um navegador, mas a parte interessante não é a linguagem - é o runtime por baixo dela. O Node combina o motor V8 do Google com uma biblioteca C chamada libuv para dar ao JavaScript, uma linguagem sem conceito nativo de threads ou I/O assíncrono, uma maneira de lidar com milhares de conexões concorrentes usando um único thread de execução.
Esse único fato - um thread JS, I/O não bloqueante em todo o resto - explica quase todas as perguntas do tipo "por que meu servidor está fazendo isso" que um desenvolvedor Node eventualmente faz: por que uma requisição lenta paralisa outras não relacionadas, por que worker_threads existem, e por que o escalonamento horizontal parece diferente aqui do que em um servidor thread-por-requisição. Entender esse modelo é a coisa de maior alavancagem que você pode aprender antes de escrever código Node em produção.
Node executa seu JavaScript em um único thread e delega I/O para o SO e um pequeno thread pool, coordenado por um event loop que retoma seu código quando o trabalho é concluído.
Por Que Importa: Explica a maior força do Node (enorme concorrência de I/O em hardware modesto) e sua maior desvantagem (uma função síncrona vinculada à CPU pode congelar todas as outras requisições).
Conceitos Chave:V8, libuv, o event loop, o thread pool, I/O não bloqueante, microtasks.
Quando Usar Este Modelo: Para raciocinar sobre picos de latência, escolher entre worker_threads/child_process/cluster, explicar a um colega por que um loop for derrubou a API, e dimensionar cargas de trabalho vinculadas ao thread pool (criptografia, compressão, algumas chamadas fs/DNS).
Limitações / Trade-offs: Ótimo para cargas de trabalho vinculadas a I/O e alta concorrência; um padrão ruim para trabalho em lote intensivo em CPU, a menos que você opte deliberadamente por paralelismo.
Tópicos Relacionados: Fases do event loop, detalhes internos do libuv, worker threads, Node vs. Bun vs. Deno.
Antes do Node (2009), JavaScript no lado do servidor não era realmente uma coisa, e a maioria das plataformas de servidor lidava com concorrência iniciando um thread (ou processo) por conexão. Esse modelo é simples de raciocinar, mas caro: threads ociosos ainda consomem memória e são agendados pelo SO, então a concorrência é limitada por quantos threads uma máquina pode suportar.
O criador do Node, Ryan Dahl, inverteu isso: em vez de um thread por conexão, o Node usa um único thread JavaScript para tudo, e nunca deixa esse thread ocioso esperando por I/O. Quando seu código pede para ler um arquivo, consultar um banco de dados ou fazer uma requisição HTTP, o Node entrega a parte da espera para o sistema operacional (ou um thread auxiliar), retorna imediatamente o controle para o thread JS e chama seu código de volta apenas quando o resultado estiver pronto.
Um modelo mental útil: imagine um único controlador de tráfego aéreo que nunca voa um avião pessoalmente. Cada decolagem e pouso (uma operação de I/O - uma leitura de arquivo, uma escrita de socket) é tratada pelos pilotos e pela equipe de solo (o SO e libuv) enquanto o controlador continua direcionando novo tráfego. No momento em que um avião pousa, o controlador recebe uma chamada de rádio e reage a ela - mas se o controlador tiver que se afastar e resolver pessoalmente um problema de matemática (trabalho síncrono de CPU), todos os outros aviões esperam até que isso seja feito. O Node é rápido em direcionar tráfego e completamente bloqueado enquanto faz aritmética.
Duas peças tornam isso possível:
V8 - o mesmo motor JavaScript que o Chrome usa. Ele compila seu JS para código de máquina, o executa e gerencia a memória/coleta de lixo. O V8 não sabe nada sobre servidores, arquivos ou sockets.
libuv - uma biblioteca C que fornece ao Node I/O assíncrono multiplataforma, timers e um thread pool. É a camada que realmente fala com as primitivas assíncronas do sistema operacional (epoll no Linux, kqueue no macOS/BSD, IOCP no Windows) e gerencia o event loop que une tudo.
O event loop não é uma fila onde você adiciona callbacks diretamente - é um conjunto fixo de fases que o libuv percorre, cada uma responsável por um tipo diferente de trabalho pendente: timers (setTimeout/setInterval), callbacks de I/O pendentes, poll (recuperando novos eventos de I/O), check (setImmediate) e callbacks de fechamento. Entre quase todas as etapas, o Node drena a fila de microtasks - Promises resolvidas e callbacks de queueMicrotask - antes de seguir em frente, que é por isso que Promise.resolve().then() executa de forma confiável antes de um setTimeout(fn, 0), mesmo que ambos pareçam "executar em breve".
A maioria dos I/Os no Node mapeia diretamente para uma API assíncrona de nível de SO, então o libuv pode entregá-la sem threads extras envolvidos - o kernel notifica o libuv, o libuv notifica o event loop, o event loop chama seu JS. Mas um pequeno número de operações não tem equivalente assíncrono no SO - algumas chamadas fs, crypto.pbkdf2, consultas DNS e compressão zlib - então o libuv as executa em um pequeno thread pool, quatro threads por padrão, dimensionado via UV_THREADPOOL_SIZE. Essa distinção importa operacionalmente: a saturação do thread principal aparece como CPU em 100%; a saturação do thread pool aparece como requisições enfileiradas silenciosamente enquanto a CPU parece normal.
Você pode observar este modelo diretamente em vez de apenas raciocinar sobre ele - perf_hooks expõe a saúde do próprio event loop como uma métrica:
Um p99 crescente aqui significa que algo - um loop síncrono, um JSON.parse enorme, um backlog no thread pool - está impedindo o único thread JS de retornar ao event loop prontamente, o que atrasa todos os outros callbacks pendentes, não apenas o lento.
Como a concorrência vem de não bloquear, o modo de falha do modelo é específico e previsível: qualquer operação síncrona vinculada à CPU bloqueia todo o processo, não apenas a requisição que a acionou. Um único loop for hashing dados, um grande JSON.parse síncrono, ou um addon nativo que chama de volta para JS de forma síncrona irá parar todas as outras requisições em andamento nesse processo, independentemente de quantos clientes estão conectados ou quão "assíncrono" o resto do código parece.
O Node deliberadamente não resolve isso para você - ele fornece ferramentas explícitas para optar pelo paralelismo quando você precisar:
Abordagem
Força
Fraqueza
Melhor Ajuste
worker_threads
Executa JS vinculado à CPU fora do thread principal, no processo
Overhead de passagem de mensagens; não isolamento completo
Hashing, processamento de imagem/dados, parsing de payloads grandes
child_process
Isolamento completo em nível de SO; pode executar ferramentas não-Node
Overhead maior; sem memória compartilhada por padrão
Executar CLIs, rodar um runtime diferente
Módulo cluster
Usa múltiplos núcleos para HTTP em uma máquina
Cada worker tem sua própria memória/event loop
Throughput multi-core sem um orquestrador
Réplicas horizontais / fila externa
Escala além de uma máquina; desacopla trabalho retentável
Salto de rede; precisa de infra (LB, fila)
Deployments em Kubernetes/nuvem, trabalhos longos ou retentáveis
É por isso também que o encaixe do Node é moldado pela carga de trabalho, não universal: ele se destaca em trabalhos vinculados a I/O e de alta concorrência - APIs, proxies, streaming, gateways em tempo real - onde a maior parte do tempo é gasta esperando por uma rede ou disco, não computando. É um padrão mais fraco para trabalho em lote intensivo em CPU (codificação de vídeo, simulações numéricas grandes) a menos que esse trabalho seja explicitamente movido para fora do thread principal. Runtimes JS mais novos como Bun e Deno mantêm o mesmo modelo fundamental de event loop (eles ainda incorporam V8 ou um motor semelhante ao V8) - eles diferem em ferramentas, tempo de inicialização e APIs integradas, não nesta forma central de concorrência; veja Node.js vs. Bun vs. Deno para essa comparação.
O modelo permaneceu notavelmente estável ao longo da história do Node: o Node 24 envia um V8 mais novo, um fetch estável integrado e um node:test aprimorado, mas a arquitetura V8-mais-libuv-mais-thread-único por baixo é inalterada desde os primeiros lançamentos do Node. Em produção, a saúde do event loop (métricas de atraso/utilização como o snippet acima) é uma das coisas de maior sinal para colocar em um dashboard, porque ela se degrada antes que as taxas de erro normalmente o façam.
"Node é single-threaded, ponto final." Apenas seu JavaScript está confinado a um thread. O thread pool do libuv, o manuseio de I/O do próprio SO e o coletor de lixo do V8 podem usar threads adicionais nos bastidores - "single-threaded" descreve onde seus callbacks rodam, não o processo inteiro.
"async/await cria um novo thread para executar o trabalho aguardado." Não cria. await apenas pausa essa função e devolve o controle para o event loop; quando a operação subjacente é concluída, a continuação é agendada como uma microtask que roda no mesmo thread JS original.
"setImmediate executa imediatamente, antes de outro trabalho pendente." Ele executa na fase de check, após os callbacks de I/O da fase de poll - ele é ordenado em relação às fases do event loop, não é um sinônimo de "o mais rápido possível".
"Mais núcleos de CPU automaticamente significam mais throughput do Node." Um único processo Node usa um núcleo para JavaScript, não importa quantos núcleos a máquina tenha. Usar os demais requer uma escolha explícita - cluster, múltiplos processos ou worker_threads.
"Se meu código usa async/await em tudo, ele não pode bloquear o event loop." Qualquer instrução síncrona dentro de uma função assíncrona ainda executa de forma síncrona. async apenas muda como o resultado da função é entregue, não se seu código pode travar o thread enquanto ele executa.
A execução JavaScript dentro de um único processo Node é single-threaded - todos os seus callbacks rodam em um thread. O libuv usa threads adicionais internamente (seu thread pool, mais o manuseio de I/O em nível de SO), mas nenhum desse trabalho visível para JavaScript roda em paralelo com seu código.
O que o libuv realmente faz?
libuv é a biblioteca C que dá ao Node seu event loop, I/O assíncrono multiplataforma (envolvendo epoll/kqueue/IOCP), timers e um thread pool para o punhado de operações sem equivalente assíncrono no SO. O V8 executa seu JavaScript; o libuv é o que torna esse JavaScript capaz de fazer I/O não bloqueante.
Por que uma requisição lenta deixa todas as outras requisições lentas?
Cada manipulador de requisição em um processo Node compartilha o mesmo único thread JavaScript. Uma seção de código síncrona e vinculada à CPU - um loop grande, um JSON.parse enorme - ocupa esse thread completamente, então nenhum callback de outro manipulador pode rodar até que ele termine, mesmo que essas outras requisições estivessem prontas para serem concluídas instantaneamente.
Quantos threads o Node realmente usa por padrão?
Um thread executa seu JavaScript. O libuv adicionalmente executa um thread pool (quatro threads por padrão, configurável via UV_THREADPOOL_SIZE) para operações como algumas chamadas fs, crypto.pbkdf2, consultas DNS e zlib - mais quaisquer threads que o coletor de lixo do V8 e o SO usam internamente.
O `await` inicia um novo thread?
Não. await pausa a função atual e devolve o controle para o event loop. Quando a operação aguardada termina, sua continuação é agendada como uma microtask que roda no mesmo thread JS original - nenhum novo thread é criado para o await em si.
Qual é a diferença real entre V8 e Node?
V8 é apenas o motor JavaScript - o mesmo que o Chrome usa - e não sabe nada sobre arquivos, sockets ou servidores. Node é V8 mais libuv (event loop, I/O assíncrono, thread pool) mais módulos integrados (fs, http, crypto, …), o carregador de módulos e o npm - as peças que transformam "um motor JS" em "um runtime de servidor".
Por que um loop vinculado à CPU pode derrubar uma API inteira, mesmo sob balanceamento de carga?
O balanceamento de carga distribui requisições entre processos (ou máquinas), mas dentro de um único processo cada requisição ainda compartilha um único thread JS. Um loop vinculado à CPU nesse thread bloqueia todas as requisições atualmente roteadas para esse processo específico - a correção é descarregar o trabalho (worker_threads), não apenas adicionar mais instâncias balanceadas por carga, a menos que você também roteie em torno do processo afetado.
Quando devo usar `worker_threads` em vez de `cluster` ou `child_process`?
worker_threads - JavaScript vinculado à CPU que precisa ficar no mesmo processo e pode compartilhar memória via SharedArrayBuffer.
cluster - usando múltiplos núcleos para tráfego HTTP em uma única máquina, com cada worker como seu próprio processo independente.
child_process - executando um programa separado ou um runtime totalmente diferente, onde o isolamento completo em nível de SO vale o overhead.
O modelo de threading do Node difere significativamente do Bun ou Deno?
Não no nível principal - ambos ainda executam JavaScript em um único thread por processo com um event loop de I/O não bloqueante. As diferenças são principalmente ferramentas, desempenho de inicialização e APIs integradas, não a forma subjacente de concorrência.
Qual é a diferença prática entre saturação do thread pool e bloqueio do thread principal?
Bloqueio do thread principal (um loop síncrono) aparece como CPU em cerca de 100% com tudo travado. Saturação do thread pool (muitas chamadas pbkdf2/fs/zlib concorrentes) aparece como requisições enfileiradas silenciosamente atrás do tamanho fixo do pool, muitas vezes com a CPU parecendo normal - os sintomas apontam para correções diferentes.
Como observar a saúde do event loop em produção?
import { monitorEventLoopDelay } from 'node:perf_hooks';const h = monitorEventLoopDelay({ resolution: 20 });h.enable();
Amostre h.percentile(99) em um intervalo e envie para seu pipeline de métricas - um atraso crescente no p99 do event loop é um dos primeiros sinais de que algo síncrono está travando o processo, muitas vezes antes que as taxas de erro se movam.
Este modelo mudou em versões recentes do Node?
Não - o Node 24 envia um V8 mais novo, um fetch estável integrado e um node:test aprimorado, mas a arquitetura V8-mais-libuv-mais-thread-JS-único é a mesma que o Node usa desde seus primeiros lançamentos.
Por que alguém escolheria Node em vez de um runtime de servidor com threads?
I/O não bloqueante é um forte padrão para cargas de trabalho de alta concorrência e vinculadas a I/O - APIs, proxies reversos, streaming, gateways em tempo real - porque conexões ociosas custam quase nada enquanto esperam. Runtimes que usam um thread (ou processo) por requisição por padrão pagam overhead de memória e agendamento para cada conexão ociosa, o que aparece como um teto de concorrência prático menor no mesmo hardware.