Server-Sent Events: streaming de dados em tempo real sem WebSocket

1. Fundamentos do SSE: quando e por que usar em vez de WebSocket

1.1. O que são Server-Sent Events: fluxo unidirecional do servidor para o cliente

Server-Sent Events (SSE) é uma tecnologia que permite ao servidor enviar dados para o cliente de forma contínua e assíncrona, utilizando uma conexão HTTP persistente. Diferentemente do WebSocket, que estabelece um canal bidirecional completo, o SSE opera exclusivamente no sentido servidor → cliente. Isso significa que o cliente não precisa enviar requisições repetidas para obter atualizações — o servidor "empurra" os dados conforme eles se tornam disponíveis.

A especificação SSE é parte do HTML5 e é suportada nativamente por todos os navegadores modernos, exceto Internet Explorer. A implementação é surpreendentemente simples: o servidor define o Content-Type como text/event-stream e mantém a conexão aberta, enviando blocos de texto formatados.

1.2. Comparação direta: SSE vs WebSocket vs Polling vs Long Polling

Característica SSE WebSocket Polling Long Polling
Direcionalidade Servidor → Cliente Bidirecional Cliente → Servidor Cliente → Servidor
Latência Baixa (eventos em tempo real) Muito baixa Alta (intervalo fixo) Média
Reconexão automática Nativa (Last-Event-ID) Manual Não se aplica Manual
Headers customizados Não suporta nativamente Suporta Suporta Suporta
Navegadores modernos Sim (excluindo IE) Sim Sim Sim

O Polling tradicional faz requisições HTTP em intervalos fixos, gerando overhead desnecessário. O Long Polling mantém a conexão aberta até que o servidor tenha dados para enviar, mas ainda requer que o cliente inicie cada ciclo. O WebSocket oferece comunicação bidirecional com baixa latência, mas é mais complexo de implementar e pode ser bloqueado por firewalls corporativos. O SSE ocupa um ponto ideal: simplicidade do HTTP com capacidade de streaming em tempo real.

1.3. Casos de uso ideais

Os cenários perfeitos para SSE incluem:
- Notificações em tempo real: alertas de sistema, mensagens de aplicativo, notificações push
- Feeds de cotação: preços de ações, criptomoedas, taxas de câmbio
- Logs em tempo real: visualização de logs de servidor, monitoramento de aplicações
- Atualizações de dashboard: métricas de sistema, KPIs, status de serviços
- Progresso de tarefas: uploads, processamentos batch, deploys

2. Arquitetura e protocolo: como o SSE funciona por baixo dos panos

2.1. O formato text/event-stream

A comunicação SSE segue um formato de texto simples, onde cada mensagem é composta por campos opcionais:

event: update
data: {"message": "Novo dado disponível", "timestamp": 1700000000}
id: 42
retry: 3000

  • event: Define o tipo do evento (opcional). Se omitido, dispara o evento message genérico no cliente.
  • data: O payload da mensagem. Pode ser múltiplas linhas.
  • id: Identificador único do evento, usado para reconexão automática.
  • retry: Tempo em milissegundos que o cliente deve esperar antes de tentar reconectar.

2.2. Reconexão automática nativa com Last-Event-ID

Uma das vantagens mais poderosas do SSE é o mecanismo de reconexão automática. Quando a conexão cai, o navegador envia automaticamente o cabeçalho Last-Event-ID com o último id recebido. O servidor pode usar esse valor para retomar o fluxo a partir do ponto de interrupção, garantindo que nenhum evento seja perdido.

2.3. Limitações intrínsecas

  • Número máximo de conexões por domínio: A maioria dos navegadores limita a 6 conexões SSE simultâneas por domínio (especificação HTTP/1.1).
  • Ausência de suporte a headers customizados: A API EventSource não permite definir headers personalizados, o que complica a autenticação.
  • Unidirecionalidade: O cliente não pode enviar dados de volta pela mesma conexão.

3. Implementação do lado do servidor: produzindo eventos em diferentes stacks

3.1. Servidor HTTP básico em Node.js (Express/Raw HTTP)

const express = require('express');
const app = express();

app.get('/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*'
  });

  let eventId = 0;
  const interval = setInterval(() => {
    eventId++;
    res.write(`id: ${eventId}\n`);
    res.write(`event: heartbeat\n`);
    res.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
  }, 5000);

  req.on('close', () => {
    clearInterval(interval);
    res.end();
  });
});

app.listen(3000);

3.2. SSE em Python (FastAPI/Starlette)

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()

async def event_generator(request: Request):
    event_id = 0
    while True:
        if await request.is_disconnected():
            break
        event_id += 1
        data = json.dumps({"event_id": event_id, "message": "Atualização em tempo real"})
        yield f"id: {event_id}\nevent: update\ndata: {data}\n\n"
        await asyncio.sleep(2)

@app.get("/events")
async def sse_endpoint(request: Request):
    return StreamingResponse(
        event_generator(request),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no"
        }
    )

3.3. Gerenciamento de múltiplos clientes

Para broadcast de eventos para múltiplos clientes, mantenha um registro de conexões ativas:

const clients = new Map();

app.get('/events', (req, res) => {
  const clientId = Date.now();
  clients.set(clientId, res);

  req.on('close', () => {
    clients.delete(clientId);
  });
});

function broadcast(event, data) {
  clients.forEach((res) => {
    res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
  });
}

4. Consumo no frontend: a API EventSource e suas armadilhas

4.1. Uso padrão

const eventSource = new EventSource('http://localhost:3000/events');

// Evento genérico (sem campo event)
eventSource.onmessage = (event) => {
  console.log('Dados recebidos:', event.data);
};

// Evento nomeado
eventSource.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  console.log('Update recebido:', data);
});

eventSource.addEventListener('heartbeat', (event) => {
  console.log('Heartbeat:', event.data);
});

4.2. Tratamento de erros e reconexão manual

eventSource.onerror = (error) => {
  console.error('Erro na conexão SSE:', error);

  if (eventSource.readyState === EventSource.CLOSED) {
    setTimeout(() => {
      // Reconexão manual com novo EventSource
      reconnect();
    }, 5000);
  }
};

4.3. Limitações do navegador

  • CORS: Requer cabeçalhos Access-Control-Allow-Origin no servidor.
  • Cookies: Não são enviados automaticamente em conexões SSE cross-origin.
  • Conexões simultâneas: Limite de 6 por domínio no HTTP/1.1.

5. Padrões avançados: estendendo o SSE para cenários complexos

5.1. SSE com autenticação via token na URL

const token = localStorage.getItem('auth_token');
const eventSource = new EventSource(`https://api.exemplo.com/events?token=${token}`);

5.2. Compressão e chunking

Para otimizar o throughput, agrupe múltiplos eventos em um único chunk:

// Servidor agrupa eventos
function sendBatch(res, events) {
  const batch = events.map(e => 
    `event: ${e.type}\ndata: ${JSON.stringify(e.payload)}\n`
  ).join('');
  res.write(batch + '\n');
}

5.3. Fallback para ambientes sem suporte

function createSSEConnection(url) {
  if (typeof EventSource !== 'undefined') {
    return new EventSource(url);
  }

  // Fallback para Long Polling
  return createLongPollingFallback(url);
}

6. Deploy, monitoramento e escalabilidade

6.1. Proxy reverso (Nginx) e configuração de buffer

location /events {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
}

6.2. Escalando conexões com Redis Pub/Sub

const redis = require('redis');
const subscriber = redis.createClient();

subscriber.subscribe('events');
subscriber.on('message', (channel, message) => {
  broadcast('update', JSON.parse(message));
});

6.3. Métricas e health check

Implemente heartbeats para monitorar conexões ativas:

setInterval(() => {
  console.log(`Conexões ativas: ${clients.size}`);
  clients.forEach((res, id) => {
    res.write(`event: heartbeat\ndata: {"alive": true}\n\n`);
  });
}, 30000);

7. Comparação final e guia de decisão

7.1. Tabela comparativa

Característica SSE WebSocket gRPC Stream GraphQL Subscriptions
Complexidade Baixa Média Alta Média
Suporte nativo Sim (navegador) Sim Biblioteca Biblioteca
Bidirecional Não Sim Sim Sim
Reconexão automática Nativa Manual Manual Manual
Headers customizados Não Sim Sim Sim

7.2. Checklist para escolha

  • Use SSE quando: Precisa de notificações unidirecionais, simplicidade de implementação, reconexão automática nativa.
  • Use WebSocket quando: Precisa de comunicação bidirecional, jogos em tempo real, aplicações colaborativas.
  • Use gRPC Stream quando: Precisa de alta performance, streaming bidirecional em microsserviços.
  • Use GraphQL Subscriptions quando: Já utiliza GraphQL e precisa de atualizações em tempo real.

7.3. Exemplo híbrido: SSE para notificações + WebSocket para chat

// SSE para notificações do sistema
const notificationSource = new EventSource('/notifications');
notificationSource.addEventListener('alert', (event) => {
  showNotification(event.data);
});

// WebSocket para chat em tempo real
const ws = new WebSocket('wss://chat.exemplo.com');
ws.onmessage = (event) => {
  displayMessage(JSON.parse(event.data));
};

Referências