MÓDULO 09 · CONCEITO 15 DE 15

WebSockets & Real-time

Quando o servidor precisa falar sem ser perguntado

Tempo de leitura ~21 min Pré-requisito 14 · Observabilidade em comunicação distribuída Próximo Módulo 10 · Observabilidade

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.

heurística do sênior

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.

armadilha em produção

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

C# — ASP.NET Core com SignalR
// 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.

Python — FastAPI com 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.

Go — gorilla/websocket com hub de fan-out
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.

armadilha de adoção

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

  1. 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-stream com id e retry configurados. No cliente, use EventSource nativo 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.
  2. 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.
  3. 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

  1. docs RFC 6455 — The WebSocket Protocol — IETF (2011). datatracker.ietf.org/doc/html/rfc6455 — O RFC original que define WebSockets. A leitura do handshake e da estrutura de frames clarifica o que proxies e load balancers precisam suportar.
  2. docs Server-Sent Events — HTML Living Standard — WHATWG. html.spec.whatwg.org/multipage/server-sent-events.html — A especificação de SSE, incluindo o protocolo text/event-stream, campos id/event/data/retry, e o comportamento de reconexão automática do EventSource.
  3. artigo WebSockets vs Server-Sent Events vs Long Polling — Ably Engineering. ably.com/blog — Comparação técnica detalhada com benchmarks reais. A equipe Ably tem experiência operacional com os três modelos em escala.
  4. artigo How we built our real-time infrastructure — Figma Engineering (2021). figma.com/blog — Caso real de editores colaborativos. Como Figma evoluiu sua infraestrutura WebSocket de uma instância a centenas de servidores com sincronização de estado distribuído.
  5. artigo Scaling WebSockets — Fanout Blog. fanoutapp.com — Arquitetura de fan-out em larga escala. Como separar o servidor de conexões (stateful) do servidor de lógica (stateless) com um pub/sub broker intermediando.
  6. docs SignalR Documentation — ASP.NET Core — Microsoft. learn.microsoft.com — Referência completa do SignalR: transports, grupos, autenticação, scale-out com Redis backplane, e JavaScript/TypeScript client.
  7. docs gorilla/websocket — Go WebSocket library. github.com/gorilla/websocket — A biblioteca WebSocket mais usada em Go. Documentação inclui exemplos de chat, broadcast, e controle de concorrência com a constraint de "um único writer por conexão".
  8. artigo 1 million WebSocket connections in Go — Eran Yanay (2017). github.com/eranyanay — Benchmark e arquitetura para WebSockets em Go de alta densidade de conexões. Técnicas de pooling de buffers e goroutines para reduzir footprint por conexão.
  9. artigo Real-time delivery architecture at Twitter — Twitter Engineering (2013). blog.twitter.com — Como o Twitter construiu sua infraestrutura de streaming (UserStreams, SiteStreams) sobre SSE/long polling com fan-out em escala de centenas de milhões de usuários.
  10. livro Designing Data-Intensive Applications — Martin Kleppmann (2017). O'Reilly. Capítulo 11 cobre stream processing e a ideia de sistemas reativos que propagam mudanças em tempo real — o contexto maior no qual WebSockets e SSE se inserem.
  11. artigo SSE vs WebSockets: A practical guide — Smashing Magazine. smashingmagazine.com — Perspectiva de frontend com código de exemplo. Bom complemento técnico para a perspectiva de infraestrutura.
  12. docs Redis Pub/Sub Documentation — Redis Ltd. redis.io/docs/manual/pubsub — Semântica de entrega at-most-once, sem persistência, sem consumer groups. Fundamental entender os limites antes de usar como backplane de WebSockets em produção.