Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Server-Sent Events no Node.js

Atualizado em: 2 de outubro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

Server-Sent Events, ou SSE, permite que um servidor envie eventos contínuos para o navegador por uma conexão HTTP. O cliente usa EventSource, enquanto o servidor responde com o tipo text/event-stream.

SSE é indicado para notificações, progresso, dashboards, logs e atualizações unidirecionais. Quando o cliente também precisa enviar mensagens frequentes pela mesma conexão, WebSocket pode ser mais adequado.

Formato de evento

id: 42
event: order.updated
data: {"orderId":123,"status":"paid"}

Uma linha vazia encerra o evento. Campos comuns:

  • data: conteúdo;
  • event: nome do evento;
  • id: identificador para reconexão;
  • retry: intervalo de reconexão sugerido.

Servidor HTTP básico

import { createServer } from 'node:http';

const server = createServer((req, res) => {
  if (req.url !== '/events') {
    res.writeHead(404).end();
    return;
  }

  res.writeHead(200, {
    'content-type': 'text/event-stream; charset=utf-8',
    'cache-control': 'no-cache',
    connection: 'keep-alive',
  });

  res.write('retry: 5000\n\n');

  const timer = setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 10_000);

  req.on('close', () => {
    clearInterval(timer);
  });
});

server.listen(3000);

O listener de close é essencial para remover timers e inscrições quando o cliente desconecta.

Cliente no navegador

const events = new EventSource('/events');

events.onmessage = (event) => {
  console.log(JSON.parse(event.data));
};

events.onerror = (error) => {
  console.error('SSE falhou', error);
};

EventSource tenta reconectar automaticamente. O servidor precisa tratar reconexões e evitar duplicação.

Eventos nomeados

res.write('event: order.updated\n');
res.write(`data: ${JSON.stringify(order)}\n\n`);

No cliente:

events.addEventListener('order.updated', (event) => {
  const order = JSON.parse(event.data);
  updateOrder(order);
});

IDs e Last-Event-ID

Quando o servidor envia id, o navegador pode informar o último evento recebido ao reconectar:

Last-Event-ID: 42

O servidor pode retomar após esse ID. Para isso, eventos precisam ser armazenados em log, banco ou fila com retenção.

Replay

const lastEventId = req.headers['last-event-id'];
const missedEvents = await eventStore.listAfter(lastEventId);

for (const event of missedEvents) {
  writeEvent(res, event);
}

Defina limite de replay. Um cliente muito atrasado pode precisar buscar um snapshot completo e então continuar pelo stream.

Função para escrever eventos

function writeEvent(res, {
  id,
  event,
  data,
  retry,
}) {
  if (id !== undefined) res.write(`id: ${id}\n`);
  if (event) res.write(`event: ${event}\n`);
  if (retry) res.write(`retry: ${retry}\n`);

  const text = typeof data === 'string'
    ? data
    : JSON.stringify(data);

  for (const line of text.split('\n')) {
    res.write(`data: ${line}\n`);
  }

  return res.write('\n');
}

Dividir linhas mantém o formato válido.

Backpressure

res.write() pode retornar false. Um cliente lento não deve acumular dados ilimitados:

if (!writeEvent(res, event)) {
  await new Promise((resolve) => res.once('drain', resolve));
}

Para milhares de clientes, não bloqueie uma rotina global aguardando cada um em sequência. Mantenha filas limitadas por conexão e desconecte consumidores excessivamente lentos.

Heartbeat

Proxies podem fechar conexões ociosas. Envie comentários periódicos:

const heartbeat = setInterval(() => {
  res.write(': heartbeat\n\n');
}, 15_000);

O intervalo deve ser menor que o timeout ocioso da infraestrutura, sem gerar tráfego excessivo.

Proxy buffering

Alguns proxies acumulam pequenos chunks e impedem eventos em tempo real. Desative buffering para a rota SSE conforme o proxy. Também evite compressão que só libera dados após formar blocos grandes.

Compressão

Compressão pode atrasar eventos. Se usar, configure flush adequado e teste. Muitas aplicações desativam compressão em text/event-stream.

CORS

Para origem diferente, configure CORS:

res.setHeader('access-control-allow-origin', 'https://app.example.com');
res.setHeader('vary', 'Origin');

Não use origem curinga quando a conexão depende de credenciais.

Autenticação

EventSource do navegador possui limitações para headers personalizados. Alternativas:

  • cookie seguro e SameSite adequado;
  • URL temporária assinada;
  • token curto em query com cuidado;
  • Fetch streaming quando headers customizados são necessários.

Tokens em query podem aparecer em logs e histórico. Prefira URL de uso único e curta duração.

Cookies

Proteja a rota contra CSRF e vazamento entre origens. Valide Origin, autenticação e autorização para cada canal.

Autorização por evento

Não basta autenticar na conexão. O servidor deve garantir que o cliente pode receber cada recurso. Não transmita um evento global e filtre apenas no frontend.

Gerenciando clientes

const clients = new Map();

function addClient(userId, res) {
  const set = clients.get(userId) || new Set();
  set.add(res);
  clients.set(userId, set);

  res.once('close', () => {
    set.delete(res);
    if (set.size === 0) clients.delete(userId);
  });
}

Imponha limites de conexões por usuário e por IP.

Broadcast

function broadcast(event) {
  for (const client of allClients) {
    if (!writeEvent(client, event)) {
      // marcar cliente lento ou limitar fila
    }
  }
}

Um loop por milhares de clientes consome CPU. Agrupe, use estruturas eficientes e faça benchmark.

Múltiplas instâncias

Se clientes estão distribuídos entre pods, eventos precisam chegar a todos. Use Redis Pub/Sub, NATS, Kafka ou outro barramento, conforme requisitos de durabilidade.

Pub/Sub simples perde mensagens durante desconexão. Para replay confiável, use log durável.

Sticky sessions

SSE é uma conexão persistente, portanto permanece no backend escolhido. Não depende de sticky para reconectar, desde que qualquer instância consiga recuperar estado e replay.

Limites de conexão

Navegadores e proxies possuem limites por origem e protocolo. HTTP/2 multiplexa conexões melhor, mas teste o ambiente real.

Graceful shutdown

Ao encerrar, pare de aceitar conexões, envie um evento de fechamento opcional, termine as respostas e permita reconexão em outra instância.

for (const res of allClients) {
  writeEvent(res, {
    event: 'server.restart',
    data: { reconnect: true },
    retry: 1000,
  });
  res.end();
}

Rate limiting

Limite novas conexões, reconexões rápidas e assinaturas. Um cliente com erro pode criar loop de reconexão. Use backoff e retry.

Observabilidade

Meça:

  • conexões abertas;
  • conexões por usuário;
  • duração;
  • reconexões;
  • eventos enviados;
  • bytes;
  • clientes lentos;
  • filas descartadas;
  • replay;
  • erros e encerramentos.

Teste

Teste reconexão, Last-Event-ID, proxy, heartbeat, cliente lento, múltiplas instâncias, autorização, shutdown e rede instável. Use uma conexão real, pois mocks simples não reproduzem buffering.

SSE ou WebSocket

Use SSE para fluxo servidor-cliente, reconexão automática e integração HTTP simples. Use WebSocket para comunicação bidirecional frequente, jogos, colaboração e protocolos customizados.

Erros comuns

  • não enviar linha vazia;
  • não limpar timers;
  • ignorar backpressure;
  • usar compressão com buffering;
  • não enviar heartbeat;
  • armazenar clientes sem remover;
  • não proteger tokens em query;
  • não suportar replay;
  • broadcast sem autorização;
  • não tratar shutdown.

Fluxo recomendado

Use text/event-stream, IDs, heartbeat e filas limitadas. Planeje replay, autenticação e múltiplas instâncias. Combine com Streams e Backpressure, Graceful Shutdown, Rate Limiting e HTTP/2.

Consulte o guia de Server-Sent Events da MDN e a especificação de SSE.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita