Tem uma frase que define quase todo bug de “tempo real” na web:
Você acha que precisa de WebSocket.
Na verdade, você só precisa parar de fazer polling errado.
A web nasceu em cima de um acordo simples: eu (cliente) pergunto, você (servidor) responde, a conexão morre.
Isso funciona tão bem que virou o padrão mental de todo dev.
Só que “tempo real” é o exato oposto disso.
Tempo real é o servidor falando sozinho.
É o sistema te cutucando quando alguma coisa aconteceu: nova mensagem, notificação, mudança de status, nova linha de log, atualização de dashboard.
A pergunta não é “qual é mais poderoso?”.
A pergunta certa é: qual é o tipo de conversa que seu app precisa ter?
Porque as três soluções existem por um motivo: cada uma resolve um “tipo de conversa” diferente.
O que muda quando você sai do HTTP normal
No HTTP clássico, você trabalha com “momentos”:
- eu faço uma request
- eu recebo uma response
- acabou
Em tempo real, você trabalha com “estado contínuo”:
- eu estou conectado
- eu continuo recebendo eventos
- a conexão cai e volta
- eu não posso perder mensagens importantes
- eu não posso derrubar meu servidor com 20 mil conexões
Isso traz três dores práticas que quase ninguém pensa no início:
A primeira: latência percebida.
Não é “quanto tempo a requisição leva”. É “quanto tempo o usuário acha que levou pra acontecer”.
A segunda: custo invisível.
Não é só CPU e RAM. É conexões abertas, timeouts, proxies no caminho, balanceadores que encerram stream, e limite de conexões simultâneas.
A terceira: semântica de evento.
“Notificação” é evento? É estado? Precisa garantir entrega? Pode perder? Pode duplicar?
A escolha entre Polling / SSE / WebSocket é, na prática, uma escolha entre esses custos e essas semânticas.
Polling: o martelo que resolve muita coisa (e quebra o resto)
Vamos desenhar o polling do jeito que ele realmente é.
Imagine um porteiro de prédio.
Você quer saber se chegou encomenda.
Você vai a cada 2 segundos na portaria e pergunta:
“Chegou algo?”
“Não.”
“E agora?”
“Não.”
“E agora?”
“Não.”
“Chegou algo?”
“Sim.”
Esse é o polling.
O polling “inocente” (e o mais comum)
1setInterval(async () => { 2 const res = await fetch('/api/status'); 3 const data = await res.json(); 4 render(data); 5}, 2000);
Funciona em qualquer lugar.
É por isso que ele nunca morre.
O problema é que a simplicidade é uma armadilha: você não vê o custo até escalar.
O custo real aparece quando você multiplica
Vamos colocar números.
10.000 usuários online.
Polling a cada 2 segundos.
Você acabou de criar 5.000 requests por segundo… mesmo quando nada acontece.
E pior: cada request carrega header, autenticação, roteamento, middleware, logging, rate limit, etc.
O custo não é só “uma query no banco”.
Polling escala mal porque ele gasta recurso quando não tem evento.
E na vida real, a maior parte do tempo… não tem evento.
O polling que dói menos (e que muita gente ignora)
Tem dois upgrades que tornam polling aceitável por mais tempo:
1) Backoff (aumentar intervalo quando não tem mudança)
Você começa rápido, e vai desacelerando quando nada acontece.
2) Long polling (segurar a request no servidor)
Você faz uma request… e o servidor só responde quando houver algo para responder (ou quando der timeout).
O long polling já começa a virar “quase SSE”, mas sem o formato de stream.
Isso aqui muda o jogo, porque reduz o número de requests vazias.
Quando polling é a escolha certa
Quando o dado muda pouco, polling é honesto e eficiente.
Deploy status a cada 30s.
Relatório que atualiza a cada 1 minuto.
Fila de processamento que não precisa ser instantânea.
Polling não é “errado”.
Ele só não pode fingir que é tempo real de alta frequência.
SSE: o streaming “puro HTTP” que resolve mais do que deveria
Agora vamos imaginar outra cena.
Ao invés de você ir na portaria de 2 em 2 segundos, você fala pro porteiro:
“Quando chegar encomenda, me chama.”
Você fica com o telefone ligado.
O porteiro te liga quando tiver novidade.
Isso é SSE.
Você abre um HTTP e deixa aberto.
O servidor vai empurrando eventos.
No browser, parece simples demais
1const es = new EventSource('/api/events'); 2 3es.onmessage = (event) => { 4 const data = JSON.parse(event.data); 5 render(data); 6};
A parte importante não é a API bonitinha.
É o que isso significa:
Você não está fazendo milhares de requests por minuto.
Você está mantendo uma conexão e recebendo eventos quando existem eventos.
Por que SSE é tão bom no mundo real
Porque ele é “real-time o suficiente” com infraestrutura “HTTP padrão”.
Ele atravessa proxy e load balancer com muito menos drama do que WebSocket na maioria dos setups.
E o navegador já cuida de reconexão automaticamente.
Ou seja: você ganha “tempo real” sem ganhar “inferno operacional”.
O que a maioria ignora: SSE é evento, não estado
SSE entrega eventos.
Se você reconectar, você pode perder o que aconteceu no meio.
Então, em SSE bom de verdade, você sempre combina:
- um endpoint de “estado atual” (pra montar a tela quando entra)
- um stream de “eventos” (pra atualizar depois)
Visualmente, fica assim:
Você entra na página → pega o estado atual (um GET normal)
Depois abre SSE → recebe alterações em tempo real
Isso é muito mais robusto do que “SSE pra tudo”.
Onde SSE brilha
Streaming de logs.
Notificações.
Dashboards ao vivo.
Feed de atividade.
Resposta streaming (tipo ChatGPT).
Na maioria dos produtos SaaS, SSE resolve 80% do “tempo real” sem WebSocket.
WebSocket: quando você precisa conversar, não só ouvir
Agora muda o tipo de conversa.
No SSE, o servidor fala e você escuta.
Mas e quando você precisa de ida e volta contínua?
Chat com “digitando…” não é só evento do servidor.
É o cliente mandando sinal o tempo todo.
Jogo multiplayer é troca constante.
Colaboração estilo Google Docs é uma tempestade de eventos indo e voltando.
Aí você entra no WebSocket.
Como isso se comporta na prática
Você abre uma conexão e mantém um canal bidirecional.
1const ws = new WebSocket('wss://exemplo.com/ws'); 2 3ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room: 'x' })); 4 5ws.onmessage = (e) => { 6 const msg = JSON.parse(e.data); 7 handle(msg); 8};
Até aqui parece “só mais uma API”.
Só que o preço vem depois.
O preço real do WebSocket (e por que ele assusta infra)
Cada usuário vira uma conexão aberta.
Isso mexe com:
- limite de conexões simultâneas
- timeouts em proxies
- sticky sessions (ou necessidade de pub/sub)
- reconexão manual e sincronização de estado
- heartbeat/ping/pong
- backpressure (cliente lento entupindo sua fila de mensagens)
E o ponto que mata muita arquitetura iniciante:
Se você tem 3 instâncias do seu backend e um cliente conectado na instância A…
como a instância B manda uma mensagem pra ele?
Resposta: você precisa de um “meio do caminho”.
Redis Pub/Sub, Kafka, RabbitMQ, NATS… alguma camada pra distribuir evento entre nós.
WebSocket raramente é “só abrir socket”.
É um sistema de eventos distribuído disfarçado.
Quando WebSocket é inevitável
- chat com presença/digitando/entrega
- jogos
- colaboração simultânea (documento, quadro, editor)
- trading/market data com altíssima frequência
- qualquer cenário onde o cliente também é produtor constante de eventos
O jeito honesto de escolher: pense no tipo de conversa
Em vez de matriz bonitinha, pensa assim:
Se o usuário só precisa “ser avisado”, você está no mundo SSE.
Se o usuário precisa “conversar em tempo real”, você está no mundo WebSocket.
Se a mudança é rara e tolera atraso, polling resolve e você dorme em paz.
Agora, vamos colocar isso em cenas, porque fica mais visual.
Cena 1: “Seu deploy terminou”
Você quer ver quando terminou, mas não precisa atualizar 30 vezes por segundo.
E você não precisa mandar nada pro servidor o tempo todo.
SSE é perfeito.
Polling a cada 10–30s também funciona, se você estiver com preguiça.
Cena 2: “Dashboard de vendas ao vivo”
Você quer ver números atualizando quando vendas entram.
Servidor empurra evento. Cliente só atualiza UI.
SSE.
Cena 3: “Chat com digitando…”
Aqui o cliente manda “estou digitando” e recebe “fulano está digitando” o tempo todo.
WebSocket.
Cena 4: “Feed de atividades”
Servidor te manda eventos. Você não precisa mandar nada.
SSE.
Cena 5: “Google Docs”
Você está enviando mudanças pequenas o tempo inteiro e recebendo mudanças de outras pessoas.
WebSocket + um monte de engenharia extra (CRDT/OT, etc).
O detalhe que decide tudo: escala e infraestrutura
Aqui é onde dev bom se separa de dev empolgado.
Porque “tempo real” não morre no código.
Ele morre no load balancer.
Coisas que derrubam real-time na prática
- Proxy encerrando conexão idle
- CDN cacheando errado endpoint de evento
- Serverless matando conexão longa
- Logs e middleware rodando como se fosse request normal
- Falta de limite de conexões por instância
- Falta de mecanismo de fanout (pub/sub)
Por isso SSE costuma ser o meio-termo mais saudável:
menos fricção infra que WS, e mais eficiência que polling.
Código “pronto pra usar” (sem virar tutorial chato)
Polling com backoff (menos agressivo)
1let delay = 2000; 2 3async function poll() { 4 try { 5 const res = await fetch('/api/status'); 6 const data = await res.json(); 7 8 render(data); 9 10 // se mudou algo, acelera; se não, desacelera 11 delay = data.changed ? 2000 : Math.min(delay * 1.5, 30000); 12 } catch { 13 delay = Math.min(delay * 2, 60000); 14 } 15 16 setTimeout(poll, delay); 17} 18 19poll();
Isso aqui sozinho já elimina a maioria dos “polling é ruim”.
SSE com estado inicial + stream (robusto)
1async function start() { 2 const snapshot = await fetch('/api/snapshot').then(r => r.json()); 3 render(snapshot); 4 5 const es = new EventSource('/api/events'); 6 7 es.onmessage = (e) => { 8 const event = JSON.parse(e.data); 9 applyEvent(event); // atualiza incremental 10 }; 11} 12 13start();
O truque é: snapshot monta tela, stream mantém viva.
WebSocket com reconexão simples
1function connect() { 2 const ws = new WebSocket('wss://exemplo.com/ws'); 3 4 ws.onopen = () => { 5 ws.send(JSON.stringify({ type: 'subscribe', channel: 'chat' })); 6 }; 7 8 ws.onmessage = (e) => handle(JSON.parse(e.data)); 9 10 ws.onclose = () => { 11 setTimeout(connect, 1000); 12 }; 13} 14 15connect();
Esse é o mínimo pra não depender de sorte.
Fechando: “mais poderoso” quase nunca é “melhor”
Polling é feio, mas resolve.
SSE é subestimado e elegante.
WebSocket é canhão — e canhão exige artilharia (infra, escala, observabilidade).
Se você quiser uma regra curta pra lembrar:
Se você só precisa empurrar updates: SSE.
Se você precisa de conversa contínua: WebSocket.
Se a atualização é rara e tolera atraso: polling bem feito.
E o resto é o que sempre foi: contexto, custo e adequação.