DevGuideTK
← Trilha Backend

Tempo real (WebSocket e SSE)

Formas de o servidor enviar dados ao cliente sem esperar um pedido, e o custo de manter conexões abertas.

Os servidores desta página foram executados e testados com curl, com Chrome e atrás de um Nginx.

Um servidor SSE, em cada linguagem

Cada versão envia um evento numerado a cada 300 ms, um pulso a cada 15 segundos e continua a partir do Last-Event-ID, quando o cliente o informa.

import http from "node:http";

http.createServer((req, res) => {
  if (req.url !== "/eventos") {
    res.statusCode = 404;
    return res.end();
  }

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    "X-Accel-Buffering": "no", // pede ao Nginx que não acumule a resposta
  });
  res.write("retry: 1000\n\n"); // o navegador reconecta após 1 s se a conexão cair

  // Continua de onde o cliente parou, se ele informar o último evento recebido
  let ultimo = Number(req.headers["last-event-id"] ?? 0);

  const eventos = setInterval(() => {
    ultimo++;
    res.write(`id: ${ultimo}\nevent: progresso\ndata: ${JSON.stringify({ pedido: 981, etapa: ultimo })}\n\n`);
  }, 300);
  const pulso = setInterval(() => res.write(": ping\n\n"), 15_000); // mantém a conexão viva

  req.on("close", () => {
    clearInterval(eventos);
    clearInterval(pulso);
  });
}).listen(3000);

O resultado, nas cinco linguagens

Cada servidor recebeu dois pedidos com curl -N: um sem cabeçalho, por cerca de 1,6 segundo, e outro com Last-Event-ID: 41, por cerca de 1 segundo.

Sem Last-Event-IDCom Last-Event-ID: 41
TypeScripteventos 1 a 5eventos 42 a 44
Python (FastAPI)eventos 1 a 6eventos 42 a 45
Goeventos 1 a 5eventos 42 a 44
Javaeventos 1 a 6eventos 42 a 45
C# (ASP.NET)eventos 1 a 6eventos 42 a 45

A diferença de um evento entre as versões vem só do momento em que cada uma envia o primeiro: algumas escrevem antes de esperar os 300 ms, outras depois. O comportamento que importa, continuar a partir do evento informado, foi o mesmo nas cinco.

O que muda de uma linguagem para outra

Como enviar cada evento na horaComo saber que o cliente saiu
TypeScriptres.write, que envia sem esperar o fimEvento close do pedido
PythonUm gerador assíncrono no StreamingResponserequest.is_disconnected()
Gohttp.Flusher e Flush() depois de cada escritar.Context().Done()
JavasendResponseHeaders(200, 0) e flush()IOException ao escrever
C#Response.Body.FlushAsync() depois de cada escritaO CancellationToken da rota

Sem o envio imediato (Flush, flush ou o equivalente), os eventos ficam no buffer do próprio servidor e só saem em blocos, o mesmo sintoma do proxy com compressão. O .NET 10 acrescenta um tipo de resultado pronto para SSE (TypedResults.ServerSentEvents), e o Spring tem o SseEmitter; os exemplos usam a forma manual para mostrar o protocolo.

O exemplo gera os eventos a partir de um contador. Num sistema real, eles viriam de uma assinatura no Redis ou de uma fila, e a retomada buscaria os eventos guardados depois do Last-Event-ID, ou mandaria o estado atual.

O cliente no navegador

const fonte = new EventSource("/eventos"); // envia os cookies do mesmo domínio

fonte.addEventListener("progresso", (evento) => {
  const dados = JSON.parse(evento.data);
  atualizarTela(dados);
});

fonte.onerror = () => {
  // O navegador já está reconectando sozinho, com Last-Event-ID.
  // Só feche se o servidor disser que não há mais nada a enviar.
};

No teste com o Chrome, esse cliente sobreviveu à queda e à volta do servidor sem nenhuma linha de reconexão: o navegador reconectou depois do intervalo definido em retry: 1000, enviou Last-Event-ID: 5 e recebeu os eventos de 6 a 11.

Atrás de um Nginx

location /eventos {
    proxy_pass http://app;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;        # ou o cabeçalho X-Accel-Buffering: no na resposta
    proxy_read_timeout 1h;      # maior que o intervalo dos pulsos
    gzip off;
}

No teste, com a compressão ligada para text/event-stream e sem o cabeçalho na resposta, o cliente não recebeu nenhum evento em 3 segundos. Com X-Accel-Buffering: no enviado pela aplicação, recebeu os 9. A configuração acima deixa a rota de eventos segura, mesmo que alguém ligue a compressão em outro lugar.

Um servidor WebSocket com pulsos

import { WebSocketServer } from "ws";

const servidor = new WebSocketServer({ port: 3000 });

servidor.on("connection", (socket) => {
  socket.vivo = true;
  socket.on("pong", () => (socket.vivo = true));

  socket.on("message", (bruto) => {
    const mensagem = JSON.parse(bruto.toString());
    if (mensagem.tipo === "digitando") {
      // repassa a todos os outros clientes conectados a esta instância
      for (const outro of servidor.clients) {
        if (outro !== socket && outro.readyState === outro.OPEN) {
          outro.send(JSON.stringify({ tipo: "digitando", usuario: mensagem.usuario }));
        }
      }
    }
  });
});

// Ping periódico: derruba conexões que pararam de responder
setInterval(() => {
  for (const socket of servidor.clients) {
    if (!socket.vivo) socket.terminate();
    socket.vivo = false;
    socket.ping();
  }
}, 30_000);

Testamos com dois clientes: a mensagem de "digitando" enviada pelo primeiro chegou ao segundo. O laço de ping resolve as conexões mortas sem aviso: quem não responder ao ping até a rodada seguinte é encerrado, liberando memória e descritores.

Este servidor repassa só aos clientes da mesma instância. Com várias cópias, a mensagem precisa passar por um canal de publicação e assinatura, como descreve o nível Soluções. No WebSocket, confira também o cabeçalho Origin na abertura da conexão (a opção verifyClient da biblioteca ws), já que o navegador envia os cookies do usuário para qualquer origem.

Lista de revisão

Escolha

  • A forma combina com a necessidade: SSE para avisos do servidor, WebSocket para troca nos dois sentidos?
  • Existe uma rota comum com o estado atual, como reserva?

Conexão

  • Há pulsos com intervalo menor que o tempo de ociosidade do balanceador?
  • Nenhum buffer ou compressão segura os eventos no caminho?
  • Conexões mortas são detectadas e encerradas?
  • O servidor envia cada evento na hora, com o flush adequado?

Escala e confiabilidade

  • Os eventos chegam aos clientes conectados em outras instâncias?
  • A reconexão retoma de onde parou, ou recebe o estado atual?
  • Os limites de descritores e de memória comportam todas as conexões?
  • Os clientes reconectam com variação aleatória, para um deploy não gerar uma onda?

Segurança

  • A conexão é autenticada na abertura e encerrada quando a credencial vence?
  • O WebSocket confere o Origin?
  • Há limites de conexões por usuário e de mensagens por segundo?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Implementar SSE com retomada

    Quero implementar Server-Sent Events na minha aplicação, com retomada depois de uma reconexão.
    
    Pergunte qual linguagem e framework eu uso.
    
    Vamos em etapas:
    1. Uma rota que envia eventos numerados, com envio imediato de cada um.
    2. O tratamento do cabeçalho Last-Event-ID para continuar de onde o cliente parou.
    3. Pulsos periódicos e a detecção do cliente que saiu.
    4. Um cliente no navegador com EventSource.
    5. Um teste derrubando o servidor e confirmando que o navegador reconecta e retoma.
    6. A configuração do proxy reverso para não acumular os eventos.
    
    Em cada etapa, me passe os comandos para testar com curl ou com o navegador.
  • Implementar um WebSocket robusto

    Quero implementar um servidor WebSocket pronto para produção.
    
    Pergunte qual linguagem e biblioteca eu uso.
    
    Explique, com código:
    1. A autenticação na abertura e a conferência do Origin.
    2. O formato das mensagens e a validação de cada uma.
    3. Pulsos com ping e pong, e o encerramento das conexões mortas.
    4. A reconexão no cliente, com intervalo crescente e variação aleatória.
    5. A publicação entre instâncias com Redis.
    6. Limites de mensagens por segundo e de tamanho de mensagem.
  • Revisar uma implementação de tempo real

    Vou colar abaixo o código de uma funcionalidade de tempo real (SSE ou WebSocket), do servidor e do cliente, e a configuração do proxy, se houver. Revise para produção.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. A forma escolhida combina com a necessidade?
    2. Há pulsos e detecção de conexões mortas?
    3. Os eventos são enviados na hora, sem buffer no servidor ou no proxy?
    4. A reconexão retoma de onde parou ou recebe o estado atual?
    5. Os eventos chegam aos clientes de outras instâncias?
    6. A autenticação acontece na abertura? O WebSocket confere o Origin?
    7. Há limites de conexões e de mensagens?
    
    Para cada problema, descreva como ele aparece em produção e mostre a correção.
    
    Código e configuração:
    [cole aqui]