Comunicação em Tempo Real em Node.js
"Tempo real" em uma API Node.js quase sempre significa uma coisa específica: o servidor precisa informar a um cliente conectado algo antes que esse cliente peça por isso.
Busque em todas as páginas da documentação
"Tempo real" em uma API Node.js quase sempre significa uma coisa específica: o servidor precisa informar a um cliente conectado algo antes que esse cliente peça por isso.
O HTTP simples nunca foi construído para isso - cada troca começa com uma requisição do cliente, e um servidor só pode responder a uma requisição que já chegou.
WebSocket, Server-Sent Events (SSE) e long-polling são três maneiras diferentes de dobrar esse modelo "primeiro a requisição" para algo que suporte envios iniciados pelo servidor, e cada um faz uma troca diferente entre direcionalidade, complexidade e compatibilidade de infraestrutura.
Noções Básicas de Tempo Real mostra o código para escolher entre eles; esta página é sobre o problema subjacente que todos eles resolvem e o custo que cada solução impõe a um processo Node quando as conexões começam a se acumular.
O modelo de requisição-resposta do HTTP significa que o servidor é fundamentalmente reativo: ele só pode falar quando é falado, respondendo a uma requisição e, em seguida, no modelo clássico, encerrando essa troca.
Isso é bom para carregar uma página, mas falha no momento em que um servidor precisa dizer a um cliente já conectado "algo mudou" sem que esse cliente tenha perguntado no último segundo.
Três mecanismos resolvem isso, cada um relaxando uma parte diferente das regras do HTTP:
data: nela ao longo do tempo, que a API EventSource do navegador transforma em eventos discretos.Uma maneira útil de visualizar a diferença: WebSocket é como abrir uma linha telefônica e deixá-la conectada para que qualquer lado possa falar quando quiser; SSE é como uma transmissão de rádio unidirecional para a qual o ouvinte sintonizou, onde apenas a estação transmite; long-polling é como ligar repetidamente para alguém e perguntar "já tem novidades?", esperando na linha até que eles tenham notícias ou desliguem.
// SSE: o servidor nunca "termina" esta resposta - ele apenas continua escrevendo
res.writeHead(200, { "Content-Type": "text/event-stream" });
res.write(`data: ${JSON.stringify({ price: 142.5 })}\n\n`);
// a conexão permanece aberta; mais chamadas res.write() seguirão depoisQual mecanismo se encaixa depende quase inteiramente da direcionalidade: o cliente precisa enviar dados pelo mesmo canal após a conexão inicial, ou ele só recebe?
A diferença mecânica mais profunda entre esses três não é o formato do fio - é o que cada um custa a um processo Node para manter aberto.
Uma requisição HTTP comum ocupa recursos do servidor apenas pela breve janela entre a chegada e a resposta; uma conexão persistente (WebSocket ou SSE) ocupa um socket, um pedaço de memória para buffers e estado por conexão, e uma entrada no registro do loop de eventos enquanto estiver aberta, quer haja ou não dados fluindo ativamente.
Esse é um modelo de recurso fundamentalmente diferente: um servidor de requisições por segundo escala com a taxa de requisições, enquanto um servidor de conexões persistentes escala com a contagem de conexões mantidas simultaneamente - um serviço com tráfego modesto, mas 50.000 conexões WebSocket abertas está sob pressão real de memória, mesmo que as mensagens sejam raras.
O loop de eventos de thread único do Node lida com isso razoavelmente bem para conexões inativas, já que um socket aberto, mas silencioso, custa memória, mas não CPU; o loop só faz trabalho quando uma mensagem realmente chega em um deles.
Onde fica caro é o fanout ativo - transmitir uma mensagem para muitas conexões ao mesmo tempo - porque serializar e escrever em cada socket é um trabalho síncrono que ocupa o mesmo thread único de onde todas as outras mensagens das conexões estão esperando.
// Transmitir é um trabalho O(n) no único thread que todas as conexões compartilham -
// esta é a parte que custa CPU, não as conexões inativas em si
for (const client of connectedClients) {
client.send(payload); // cada chamada executa serialmente no mesmo loop de eventos
}Essa única linha é o motivo pelo qual "quantas conexões o Node pode suportar" e "quão rápido o Node pode transmitir para todas elas" são duas perguntas diferentes com duas respostas diferentes - a contagem de conexões inativas é limitada principalmente pela memória, mas a taxa de transferência de transmissão é limitada pelo custo de trabalho síncrono de cada send(), multiplicado pelo número de destinatários.
A autenticação também tem sua própria complicação mecânica aqui: como uma conexão persistente não é uma nova requisição a cada vez, as credenciais precisam ser verificadas uma vez, no momento da conexão ou do upgrade - um WebSocket não tem um "cabeçalho de autorização" por mensagem como o HTTP, então um socket não autenticado que tem permissão para se conectar primeiro e ser verificado depois é uma lacuna real e explorável.
Um único processo Node não é onde os sistemas em tempo real permanecem por muito tempo, uma vez que precisam escalar além de uma instância, e é aí que surge um segundo problema mais difícil: fanout entre processos.
Se dois usuários no mesmo canal de chat se conectam a duas instâncias Node diferentes atrás de um balanceador de carga, uma mensagem de um precisa de alguma forma chegar ao outro - as próprias conexões WebSocket são locais ao processo, então a instância A não tem como escrever diretamente em um socket que a instância B está mantendo.
A solução padrão é um barramento de mensagens compartilhado (Redis pub/sub é a escolha comum nesta pilha) ao qual cada instância se inscreve: a instância A publica a mensagem uma vez, e todas as instâncias - incluindo a B - a recebem e a encaminham para quaisquer de seus próprios sockets locais que se importam.
Essa única adição muda o modo de falha de todo o sistema: a contagem de conexões não precisa mais de roteamento pegajoso para "funcionar corretamente", mas o próprio barramento de mensagens se torna uma nova dependência que precisa permanecer ativa, e as garantias de ordenação/entrega de mensagens agora são tão fortes quanto o barramento fornece.
Escalando Tempo Real cobre esse padrão - sessões pegajosas, adaptadores Redis e os compromissos operacionais - em profundidade; o ponto neste nível é reconhecer por que uma solução de processo único para de ser suficiente bem antes que a contagem bruta de conexões seja o gargalo.
Bibliotecas de nível superior existem especificamente para absorver essa complexidade: Socket.IO agrupa reconexão automática, fanout baseado em salas e um adaptador Redis exatamente para este problema entre instâncias, ao custo de um protocolo personalizado em camadas sobre WebSocket (ou um transporte de fallback).
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
WebSocket Bruto (ws) | Controle total, sobrecarga mínima, protocolo padrão | Você constrói reconexão, salas e fanout por conta própria | Protocolos personalizados, aplicativos bidirecionais críticos para latência |
| Server-Sent Events | HTTP simples - funciona através da maioria dos proxies/balanceadores de carga sem modificação, reconexão automática do navegador | Apenas do servidor para o cliente; nenhum canal do cliente para o servidor na mesma conexão | Feeds ao vivo, dashboards, fluxos de notificação |
| Long-polling | Funciona em qualquer lugar onde o HTTP simples funciona, incluindo ambientes de rede hostis | Maior latência, churn constante de conexão, mais carga no servidor por mensagem | Caminho de fallback apenas, raramente uma primeira escolha hoje |
| Socket.IO | Reconexão, salas e fanout multi-instância integrados | Protocolo personalizado - não interoperável com clientes WebSocket simples; peso de dependência adicionado | Equipes que desejam que o problema de escalonamento seja resolvido em grande parte imediatamente |
Entrega iniciada pelo servidor: levar dados a um cliente já conectado sem que esse cliente precise pedi-los novamente. O HTTP simples suporta apenas o inverso - um cliente pedindo e um servidor respondendo.
O modelo de requisição-resposta do HTTP exige que uma requisição exista antes que uma resposta possa ser enviada - não há canal para o servidor escrever por iniciativa própria, a menos que algo (um upgrade, uma resposta mantida aberta ou polling repetido) mude isso.
Começa como uma - uma requisição HTTP com um cabeçalho Upgrade - mas depois que o servidor responde 101 Switching Protocols, ambos os lados param de falar HTTP completamente e trocam mensagens emolduradas brutas sobre o mesmo socket TCP enquanto ele estiver aberto.
SSE é definido como parte da API EventSource do navegador, que tem comportamento de reconexão integrado especificado como parte do padrão. WebSocket é um protocolo de nível mais baixo sem tal contrato de API - a reconexão é deixada inteiramente para o código da aplicação.
Não por si só - conexões inativas consomem principalmente memória (buffers de socket e estado por conexão), e o loop de eventos não faz trabalho em uma conexão até que uma mensagem realmente precise ser enviada ou recebida nela.
Geralmente volume de mensagens, especificamente fanout de broadcast - enviar para muitas conexões ao mesmo tempo é trabalho síncrono no único thread do loop de eventos, então ele compete com as mensagens de todas as outras conexões, enquanto conexões inativas sozinhas custam principalmente memória.
Cada conexão WebSocket é mantida pela instância específica que a aceitou - não há como um processo Node escrever em um socket que um processo diferente está mantendo, e é por isso que sistemas em tempo real entre instâncias precisam de um barramento de mensagens compartilhado.
Raramente para novos sistemas - principalmente como um fallback quando a infraestrutura (certos proxies corporativos) bloqueia conexões persistentes completamente. Custa mais latência e carga no servidor por mensagem do que as alternativas.
Não - é um protocolo personalizado em camadas sobre WebSocket (com um transporte de fallback para ambientes que o bloqueiam), e é por isso que um cliente WebSocket simples não pode falar com um servidor Socket.IO sem a biblioteca cliente correspondente.
No momento da conexão ou upgrade, antes que quaisquer mensagens sejam aceitas - uma conexão persistente não tem um equivalente por mensagem do cabeçalho Authorization do HTTP, então verificar a autenticação "após a primeira mensagem" deixa uma janela real onde um socket não autenticado já está conectado.
Somente quando você estiver executando mais de uma instância de servidor - um recurso em tempo real de processo único pode transmitir diretamente para suas próprias conexões locais. Uma camada pub/sub compartilhada só se torna necessária quando uma mensagem de uma instância precisa alcançar um cliente conectado a outra.
Versões da Stack: Esta página foi escrita para Node.js 24 LTS, npm 10+ e TypeScript 5.6+.
Revisado por Chris St. John·Última atualização: 15 de jul. de 2026