HTTP é um protocolo de request-response. O cliente pergunta; o servidor responde; a conexão se encerra. Esse modelo serve bem a noventa por cento das interações de uma aplicação web — carregar uma página, enviar um formulário, consultar uma API. Mas falha para um conjunto específico de problemas: o servidor precisa notificar o cliente de algo que aconteceu depois que a conexão original foi encerrada. Um preço de ação mudou. Uma mensagem chegou. Um job completou. Uma posição em um mapa se moveu.
A solução mais ingênua — polling — faz o cliente perguntar repetidamente: "tem algo novo?" Em intervalos de um segundo ou cinco segundos, o cliente dispara uma requisição, recebe "não" na maioria das vezes, e repete. É simples de implementar, fácil de entender, e profundamente ineficiente: a maioria das respostas não carrega informação nova, e a latência de entrega é limitada pelo intervalo de poll. Para sistemas com muitos usuários simultâneos e baixa frequência de eventos, polling gera carga desnecessária no servidor; para sistemas com alta frequência de eventos, o intervalo precisa ser curto o suficiente para parecer real-time, o que piora o problema de carga.
A evolução técnica produziu três soluções progressivamente mais sofisticadas: long polling, Server-Sent Events (SSE) e WebSockets. Cada uma resolve uma versão do problema com trade-offs diferentes de complexidade, compatibilidade, escalabilidade e direcionalidade. Escolher entre elas é uma decisão de arquitetura com consequências que vão além do cliente — afetam como o sistema escala horizontalmente, como lidar com reconexão, como autenticar a conexão persistente, e como monitorar o estado de cada canal aberto.
Este conceito cobre o espectro completo de comunicação em tempo real: de polling a WebSockets, a mecânica de cada abordagem, os problemas de escala que surgem com conexões persistentes, e as arquiteturas que resolvem esses problemas com pub/sub brokers.
Short polling e long polling
Short polling é o ponto de partida histórico — e ainda válido em casos onde a latência de
alguns segundos é aceitável e o volume de clientes é baixo. O cliente faz requisições
periódicas, o servidor responde imediatamente com o estado atual (ou "sem mudanças"), e a
conexão se encerra. Implementação: um setInterval no cliente chamando um endpoint
REST. A simplicidade é a vantagem; a ineficiência e a latência mínima igual ao intervalo
são as desvantagens.
Long polling resolve a latência: o cliente faz uma requisição e o servidor a segura aberta até ter algo novo para enviar — ou até um timeout (tipicamente 30–60 segundos). Quando o servidor envia a resposta, o cliente a processa e imediatamente abre uma nova conexão long poll. Do ponto de vista do usuário, a entrega é quasi-imediata. Do ponto de vista do servidor, cada cliente consome uma thread ou conexão enquanto aguarda — o que se torna problema de escala com muitos usuários simultâneos antes da proliferação de servidores event-driven e frameworks async. Long polling foi a base de sistemas de chat em tempo real como o primeiro Facebook Chat (2008) e Meebo, antes de WebSockets existirem como padrão.
Server-Sent Events (SSE)
SSE é um padrão do W3C (2015, incorporado ao HTML Living Standard) que permite ao servidor
enviar um stream unidirecional de eventos sobre uma conexão HTTP convencional. O cliente
abre a conexão com Accept: text/event-stream e o servidor mantém o body aberto,
enviando eventos formatados à medida que acontecem. Não há WebSocket handshake, não há
protocolo binário, não há biblioteca cliente obrigatória — o browser implementa a API
EventSource nativamente, e o servidor é apenas um HTTP server que não fecha
o body.
// Formato do stream text/event-stream (servidor → cliente)
// Cada evento é separado por linha em branco (\n\n)
id: 1
event: price-update
data: {"symbol":"PETR4","price":38.42}
id: 2
event: price-update
data: {"symbol":"VALE3","price":71.85}
// Retry automático: o cliente reconecta enviando o último id recebido
// no header Last-Event-ID — o servidor pode retomar do ponto correto.
retry: 3000
O campo id permite reconexão com entrega from-last-position: se a conexão cair,
o browser reconecta automaticamente enviando o Last-Event-ID do último evento
recebido, e o servidor pode retomar o stream a partir daí. O campo retry controla
o intervalo de reconexão em milissegundos. O campo event permite múltiplos tipos
de evento no mesmo stream, com handlers diferentes no cliente via addEventListener('price-update', ...).
SSE é a solução certa quando a comunicação é essencialmente unidirecional: o servidor envia
dados ao cliente, mas o cliente não precisa enviar dados pelo mesmo canal (ainda pode fazer
requisições REST normais). Dashboards ao vivo, feeds de preços, progresso de jobs longos,
notificações push, comentários em tempo real onde o usuário escreve pelo formulário e recebe
respostas via SSE. A compatibilidade é excelente — todos os browsers modernos suportam
EventSource, e qualquer HTTP/1.1 server funciona sem configuração adicional.
Se a comunicação é unidirecional — servidor envia, cliente consome — prefira SSE a
WebSockets. SSE é HTTP puro: funciona através de proxies, load balancers e CDNs sem
configuração especial, suporta reconexão automática nativa, e é trivial de debugar com
curl. WebSockets resolvem um problema diferente.
WebSockets em profundidade
WebSockets (RFC 6455, 2011) estabelecem um canal de comunicação full-duplex bidirecional sobre uma única conexão TCP. Diferente de SSE e polling, o protocolo é distinto do HTTP — usa o HTTP apenas para o handshake inicial de upgrade, depois muda para o protocolo WS. Tanto o cliente quanto o servidor podem enviar mensagens a qualquer momento, sem que o outro lado precise ter feito uma requisição.
O handshake começa com uma requisição HTTP com os headers Upgrade: websocket e
Connection: Upgrade, mais um nonce em Sec-WebSocket-Key. O servidor
responde com 101 Switching Protocols e o Sec-WebSocket-Accept
calculado — e a partir desse ponto a conexão deixa de ser HTTP e passa a ser WebSocket.
Frames podem ser text (UTF-8), binary, ping, pong, ou close. Ping/pong é o mecanismo de
keepalive — o servidor envia pings periódicos e o cliente responde com pong; se não houver
resposta, a conexão é considerada morta e fechada.
// Handshake HTTP → WebSocket
GET /ws HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
// A partir daqui: frames WS bidirecionais sobre TCP
Autenticação em WebSockets merece atenção especial. O header Authorization
não é enviado automaticamente pelo browser no upgrade request — diferente das requisições
HTTP normais. As abordagens comuns são: passar o token como query parameter no URL da
conexão (wss://api.example.com/ws?token=... — simples, mas o token pode
aparecer em logs de servidor), autenticar via cookie (funciona bem com cookies HttpOnly,
seguro, mas requer CORS configurado), ou autenticar via primeira mensagem após a conexão
ser estabelecida (o servidor segura a conexão em estado "não autenticado" até receber
um frame com o token, e fecha a conexão se o timeout for atingido sem autenticação).
O problema de escala: conexões persistentes e horizontal scaling
Conexões HTTP convencionais são stateless e efêmeras — qualquer instância do servidor pode atender qualquer requisição. WebSockets e SSE quebram essa propriedade: a conexão é persistente e vive em uma instância específica. Se você escala horizontalmente para três instâncias do servidor, as conexões abertas na instância A não têm acesso às que estão na instância B. Um evento que precisa ser entregue para todos os clientes conectados não pode ser enviado por apenas uma instância.
Sticky sessions (afinidade de sessão no load balancer) resolvem o roteamento — o mesmo cliente sempre cai na mesma instância — mas não resolvem o problema de fan-out: eventos que precisam ir para múltiplos clientes em instâncias diferentes. A solução arquitetural correta é um pub/sub broker como intermediário: cada instância do servidor assina os tópicos relevantes, e quando precisa enviar um evento, publica no broker; o broker entrega para todas as instâncias assinantes, que repassam para seus clientes conectados.
// Arquitetura de fan-out com Redis Pub/Sub
//
// Cliente A ──────────────┐
// Cliente B ──────────────┤ Instância 1 ─── subscribe("notifications") ──┐
// Cliente C ──────────────┘ │
// Redis
// Cliente D ──────────────┐ │
// Cliente E ──────────────┤ Instância 2 ─── subscribe("notifications") ──┘
// Cliente F ──────────────┘
//
// Quando um evento acontece: qualquer instância publica no Redis,
// que entrega para todas as instâncias assinantes,
// que enviam para seus clientes conectados via WS ou SSE.
Redis Pub/Sub é a solução mais comum para fan-out em escala pequena e média — latência sub-milissegundo, operação simples, amplamente suportado por bibliotecas. Para escala maior ou garantias de entrega (Redis Pub/Sub é at-most-once, sem persistência), alternativas como NATS, Ably, Pusher ou um tópico Kafka são mais adequadas. Serviços gerenciados como Ably e Pusher abstraem toda essa infraestrutura, incluindo reconexão, presença de usuário e histórico de mensagens — ao custo de dependência de vendor e custo por mensagem.
WebSockets e proxies reversos têm fricção histórica. Nginx precisa de configuração explícita
para upgrade (proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade;).
Timeouts de proxy que encerram conexões ociosas — comum em configurações padrão de 60s —
quebram conexões WebSocket sem aviso. ALBs da AWS encerram conexões WS após 3600 segundos
independente de atividade. Mapeie todos os proxies na sua infra antes de colocar WebSockets
em produção e teste comportamento de reconnect sob condições de timeout.
Implementação nas três linguagens
// SignalR abstrai WebSockets, SSE e long polling automaticamente,
// com fallback progressivo e reconexão built-in.
// Hub: define o contrato de mensagens servidor ↔ cliente
public class NotificationHub : Hub
{
public async Task Subscribe(string topic)
{
await Groups.AddToGroupAsync(Context.ConnectionId, topic);
}
// Chamado pelo cliente
public async Task Ping() =>
await Clients.Caller.SendAsync("Pong");
}
// Envio server-side para um grupo (fan-out)
public class NotificationService(IHubContext<NotificationHub> hub)
{
public Task BroadcastAsync(string topic, object payload) =>
hub.Clients.Group(topic)
.SendAsync("Message", payload);
}
// Startup
builder.Services.AddSignalR();
app.MapHub<NotificationHub>("/hub/notifications");
// Cliente JavaScript
const conn = new signalR.HubConnectionBuilder()
.withUrl("/hub/notifications")
.withAutomaticReconnect()
.build();
conn.on("Message", (payload) => console.log(payload));
await conn.start();
await conn.invoke("Subscribe", "orders");
SignalR gerencia transporte, reconnect, grupos e autenticação via ASP.NET Core Identity.
Para escala horizontal, adicione AddStackExchangeRedis como backplane — o
Redis faz o fan-out entre instâncias automaticamente. Para WebSocket puro sem SignalR,
use HttpContext.WebSockets.AcceptWebSocketAsync() com
System.Net.WebSockets.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Dict, Set
import asyncio, json
app = FastAPI()
# Gerenciador de conexões por tópico
class ConnectionManager:
def __init__(self):
self.topics: Dict[str, Set[WebSocket]] = {}
async def subscribe(self, ws: WebSocket, topic: str):
self.topics.setdefault(topic, set()).add(ws)
def unsubscribe(self, ws: WebSocket, topic: str):
if topic in self.topics:
self.topics[topic].discard(ws)
async def broadcast(self, topic: str, message: dict):
sockets = self.topics.get(topic, set()).copy()
dead = set()
for ws in sockets:
try:
await ws.send_json(message)
except Exception:
dead.add(ws)
self.topics[topic] -= dead
manager = ConnectionManager()
@app.websocket("/ws/{topic}")
async def websocket_endpoint(ws: WebSocket, topic: str):
await ws.accept()
await manager.subscribe(ws, topic)
try:
while True:
data = await ws.receive_text()
# eco ou processamento de mensagem do cliente
except WebSocketDisconnect:
manager.unsubscribe(ws, topic)
# SSE alternativo para streams unidirecionais
from fastapi.responses import StreamingResponse
import asyncio
@app.get("/events/{topic}")
async def sse_endpoint(topic: str):
async def event_stream():
event_id = 0
while True:
await asyncio.sleep(1)
event_id += 1
yield f"id: {event_id}\ndata: {{}}\n\n"
return StreamingResponse(event_stream(),
media_type="text/event-stream")
FastAPI suporta WebSockets nativamente via Starlette. Para escala horizontal com Redis,
use broadcaster (biblioteca async de pub/sub) ou integre Redis Pub/Sub
com aioredis diretamente. Para SSE em produção, gerencie o loop de eventos
com cuidado — conexões SSE são coroutines de longa duração que precisam de timeout
e cleanup explícito.
package main
import (
"net/http"
"sync"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
type Hub struct {
mu sync.RWMutex
clients map[string]map[*websocket.Conn]struct{}
}
func NewHub() *Hub {
return &Hub{clients: make(map[string]map[*websocket.Conn]struct{})}
}
func (h *Hub) Subscribe(topic string, conn *websocket.Conn) {
h.mu.Lock()
defer h.mu.Unlock()
if h.clients[topic] == nil {
h.clients[topic] = make(map[*websocket.Conn]struct{})
}
h.clients[topic][conn] = struct{}{}
}
func (h *Hub) Broadcast(topic string, msg []byte) {
h.mu.RLock()
defer h.mu.RUnlock()
for conn := range h.clients[topic] {
conn.WriteMessage(websocket.TextMessage, msg)
}
}
func (h *Hub) HandleWS(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
defer conn.Close()
topic := r.URL.Query().Get("topic")
h.Subscribe(topic, conn)
for {
if _, _, err := conn.ReadMessage(); err != nil {
break // cliente desconectou
}
}
}
gorilla/websocket é a biblioteca WebSocket mais usada em Go — o pacote
nativo golang.org/x/net/websocket é desaconselhado pela própria equipe
Go. Para escala horizontal, substitua o map em memória por Redis Pub/Sub com
go-redis — cada instância assina os tópicos e repassa para seus conns
locais. Para SSE puro, use w.Header().Set("Content-Type", "text/event-stream")
com http.Flusher.
Matriz de decisão: qual modelo usar
A escolha entre polling, SSE e WebSockets é determinada por três variáveis: direcionalidade (o servidor envia, o cliente envia, ou ambos?), frequência de eventos (esparsos ou contínuos?), e requisitos de infra (proxies legacy, CDN, compatibilidade de cliente).
Short polling é adequado quando a latência de alguns segundos é tolerável, o número de clientes simultâneos é baixo (centenas, não milhares), e a simplicidade de implementação e operação tem mais valor do que a eficiência. Alertas de disponibilidade de produto, status de build em CI, dashboards internos sem SLA de latência.
SSE é adequado quando o fluxo é essencialmente unidirecional (servidor → cliente), a reconexão automática é valiosa (o browser a implementa nativamente), e a infraestrutura inclui proxies que podem não suportar WebSockets. Feeds de preço, progresso de jobs, notificações push, live feeds editoriais. A maioria dos casos de "real-time" em aplicações web se encaixa aqui.
WebSockets são adequados quando a comunicação é genuinamente bidirecional e a latência precisa ser mínima: jogos multiplayer, editores colaborativos (Google Docs-style), chat com typing indicators, trading com entrada de ordens em tempo real. Se o cliente não precisa enviar dados pelo canal real-time — apenas receber — SSE é provavelmente mais simples.
WebSockets são frequentemente escolhidos porque parecem mais "técnicos" ou "modernos", não porque o problema exige bidirecionalidade. O custo operacional é real: configuração de proxy, keepalives, reconexão no cliente, escala horizontal com backplane. SSE resolve 80% dos casos de real-time com 20% da complexidade.
Observabilidade de conexões persistentes
Conexões persistentes são mais difíceis de monitorar do que requisições HTTP efêmeras. Métricas essenciais incluem: número de conexões ativas por instância (para detectar vazamentos e dimensionar capacity), taxa de reconexões (alta taxa indica instabilidade de rede ou timeouts de proxy mal configurados), latência de entrega de mensagem (diferença entre o evento acontecer no servidor e ser recebido pelo cliente), e tamanho de backlog por tópico (mensagens enfileiradas aguardando entrega — crescimento constante indica consumidores lentos ou desconectados).
Instrumentação com OpenTelemetry requer atenção: o modelo de span HTTP (requisição → resposta) não mapeia bem para conexões WebSocket de longa duração. A prática comum é criar um span para o handshake e emitir eventos (log records com trace context) para cada mensagem relevante, em vez de um span por mensagem — o que geraria spans com duração de milissegundos dentro de uma conexão de horas.
Como praticar
-
Construa um feed de preços com SSE. Implemente um servidor que gera
preços simulados de ativos financeiros e os envia via
text/event-streamcomideretryconfigurados. No cliente, useEventSourcenativo e valide que a reconexão automática retoma do último evento após queda de conexão. Compare a carga no servidor com uma implementação de polling a cada segundo com o mesmo número de clientes. - Implemente um chat simples com WebSockets e fan-out via Redis. Crie duas instâncias do servidor, conecte clientes em instâncias diferentes, e valide que mensagens enviadas por um cliente chegam aos clientes na outra instância via Redis Pub/Sub. Adicione métricas de conexões ativas e mapeie o comportamento quando uma instância reinicia.
- Mapeie o comportamento de proxies. Configure Nginx como reverse proxy na frente de um servidor WebSocket, sem e com as diretivas de upgrade explícitas. Observe o que acontece com conexões WS quando o timeout padrão do Nginx é atingido. Implemente keepalives (ping/pong) com intervalo inferior ao timeout do proxy e valide que a conexão permanece ativa.
Referências para aprofundar
- docs RFC 6455 — The WebSocket Protocol — IETF (2011).
- docs Server-Sent Events — HTML Living Standard — WHATWG.
- artigo WebSockets vs Server-Sent Events vs Long Polling — Ably Engineering.
- artigo How we built our real-time infrastructure — Figma Engineering (2021).
- artigo Scaling WebSockets — Fanout Blog.
- docs SignalR Documentation — ASP.NET Core — Microsoft.
- docs gorilla/websocket — Go WebSocket library.
- artigo 1 million WebSocket connections in Go — Eran Yanay (2017).
- artigo Real-time delivery architecture at Twitter — Twitter Engineering (2013).
- livro Designing Data-Intensive Applications — Martin Kleppmann (2017).
- artigo SSE vs WebSockets: A practical guide — Smashing Magazine.
- docs Redis Pub/Sub Documentation — Redis Ltd.