SSE para eventos em tempo real, sem WebSocket
Equipe AltiviaCloud · 5 de setembro de 2026 · 3 min de leitura
O servidor empurra evento pelo HTTP de sempre e o navegador já sabe receber. Onde o SSE ganha do polling, onde ele não serve, e os dois detalhes que só quebram em produção.
Quando o usuário está olhando a tela esperando um código chegar, cada segundo conta. Consultar a API em laço funciona, mas gasta requisição e ainda assim atrasa. WebSocket resolve, mas traz uma conexão bidirecional que você não precisa — e que não sobrevive bem em ambiente serverless.
Server-Sent Events fica no meio: o servidor empurra evento pelo mesmo HTTP de sempre, e o navegador já sabe fazer isso sem biblioteca nenhuma.
O lado do cliente é uma API só
const stream = new EventSource(`/api/orders/${pedidoId}/stream`);
stream.onmessage = (evento) => {
const dados = JSON.parse(evento.data);
if (dados.event === "sms:received") {
mostrarCodigo(dados.data.code);
stream.close();
}
};
stream.onerror = () => {
// o navegador reconecta sozinho; só trate se quiser desistir
};
Sem dependência, sem handshake, sem protocolo próprio. E a reconexão é automática: se a conexão cair, o navegador tenta de novo sozinho, o que é mais do que a maioria das implementações caseiras de WebSocket faz.
Não esqueça o close(). EventSource mantém a conexão aberta indefinidamente — inclusive depois que a página já mostrou o que precisava.
O lado do servidor
O essencial é responder com text/event-stream e ir escrevendo:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const enviar = (dados: unknown) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(dados)}\n\n`));
};
enviar({ type: "connected" });
const cancelar = assinarEventos(pedidoId, enviar);
// fecha quando o cliente desiste
return () => cancelar();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
Dois detalhes que quebram em produção e não em desenvolvimento:
As duas quebras de linha no fim. data: ...\n\n — uma só não fecha o evento, e o cliente fica esperando o resto para sempre.
no-transform no Cache-Control. Sem isso, um proxy no caminho pode decidir bufferizar a resposta, e aí os eventos chegam todos juntos no final. O sintoma é cruel: funciona local, "atrasa" em produção.
Onde o SSE não serve
Quando ninguém está olhando. SSE vive na aba do navegador. Fechou a aba, perdeu o evento — não há reentrega, não há histórico. Para registrar em banco, processar em fila ou reagir do lado do servidor, o caminho é webhook, que sobrevive à aba fechada.
Quando o cliente precisa falar. SSE é mão única, do servidor para o cliente. Se o cliente também envia, ou você usa requisições normais para isso, ou o caso pede WebSocket de verdade.
Conexão longa em função serverless. Cada stream aberto ocupa uma execução, e execução tem teto de duração. Para uma espera de vinte minutos, avalie o custo — ou combine: SSE para a janela curta em que o usuário está olhando, webhook para o resto.
O padrão que funciona na prática
Os dois juntos, cada um no seu papel:
- SSE enquanto a pessoa está na tela, para o código aparecer no instante em que chega.
- Webhook para o seu servidor registrar o que aconteceu, com ou sem alguém olhando.
Uma armadilha nessa combinação: se a tela escuta pelos dois canais ao mesmo tempo e simplesmente adiciona o que recebe, a mesma mensagem entra duas vezes na lista. Deduplique por identificador do evento, não pela ordem de chegada — o mesmo cuidado de idempotência que o webhook exige, agora na interface.
Se for consultar mesmo assim
Nem todo caso justifica stream. Se você vai perguntar em laço, dois cuidados:
- Intervalo de alguns segundos, não de milissegundos. Perguntar mais rápido não faz a operadora entregar antes, só consome sua cota até tomar 429.
- Limite o laço pelo
expiresAtque a API devolve, não por uma contagem fixa de tentativas.
O ganho do SSE aqui não é performance de servidor: é que a resposta aparece quando o evento acontece, e não no próximo intervalo do seu relógio.
É o mesmo canal que o painel dos números virtuais usa para mostrar o código assim que ele chega.