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

Server-Sent Events com Node.js

Atualizado em: 26 de julho de 2026

Servidores transmitindo eventos em tempo real com Node.js

Aplicações modernas frequentemente precisam mostrar informações assim que elas mudam. Um painel pode atualizar o andamento de uma tarefa, uma loja pode avisar que o pagamento foi confirmado e um sistema de monitoramento pode exibir novos eventos sem exigir que o usuário recarregue a página. Para fluxos desse tipo, Server-Sent Events com Node.js oferece uma solução simples para enviar atualizações contínuas do servidor ao navegador.

Diferentemente de uma requisição HTTP tradicional, em que o cliente recebe uma resposta e a conexão termina, o Server-Sent Events mantém a conexão aberta. O servidor escreve novos eventos sempre que necessário, enquanto o navegador os processa por meio da API EventSource. Neste guia, você aprenderá a criar um endpoint SSE, transmitir mensagens, reconectar clientes, tratar desconexões, proteger a rota, testar o fluxo e preparar a arquitetura para múltiplas instâncias.

O que é Server-Sent Events?

Server-Sent Events, também chamado de SSE, é um padrão baseado em HTTP para comunicação unidirecional em tempo real. O servidor envia eventos para o cliente, mas o cliente não usa a mesma conexão para enviar mensagens de volta. Quando precisa enviar dados ao servidor, ele continua usando requisições HTTP normais, como fetch, formulários ou chamadas de API.

Essa característica torna o SSE adequado para notificações, progresso de tarefas, placares, feeds, atualizações de estoque, telemetria e dashboards. O formato usa o tipo de conteúdo text/event-stream e transmite blocos de texto separados por uma linha em branco.

Se você ainda está consolidando os fundamentos, revise o que é Node.js, o que é JavaScript e o que é uma API.

SSE ou WebSocket?

SSE e WebSocket permitem atualizações em tempo real, mas resolvem problemas diferentes. WebSocket cria um canal bidirecional: cliente e servidor podem enviar mensagens pela mesma conexão. SSE é unidirecional e aproveita o funcionamento normal do HTTP.

Use SSE quando a maior parte do tráfego parte do servidor para o navegador. Exemplos incluem acompanhar uma exportação, receber notificações, atualizar métricas e mostrar mudanças de status. Para chats, jogos, colaboração simultânea ou controle remoto com mensagens frequentes nos dois sentidos, WebSocket costuma ser mais apropriado.

O artigo sobre WebSocket com Node.js mostra um cenário bidirecional. Comparar os dois modelos ajuda a evitar uma solução mais complexa do que o problema exige.

Como funciona o formato de eventos?

Uma mensagem SSE simples possui o campo data:

data: Olá, navegador!

A linha em branco encerra o evento. Também é possível definir nome, identificador e intervalo sugerido para reconexão:

event: order-updated
id: 842
retry: 5000
data: {"orderId":842,"status":"paid"}

O campo event permite criar tipos diferentes de mensagem. O campo id ajuda o cliente a informar qual foi o último evento recebido quando ocorre uma reconexão. Já retry sugere quantos milissegundos o navegador deve esperar antes de tentar novamente.

A documentação oficial do EventSource no MDN explica a API do navegador, enquanto a especificação de Server-Sent Events detalha o formato do protocolo.

Criando o projeto Node.js

Crie uma aplicação Express:

mkdir sse-node
cd sse-node
npm init -y
npm install express

Crie o arquivo server.js:

const express = require('express');

const app = express();
app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ ok: true });
});

app.listen(3000, () => {
  console.log('Servidor em http://localhost:3000');
});

Esse projeto será expandido com uma rota que mantém a conexão aberta. Para uma estrutura mais completa, consulte o guia sobre como criar uma API com Node.js.

Criando o primeiro endpoint SSE

Adicione a rota abaixo:

app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();

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

  const timer = setInterval(() => {
    const payload = {
      time: new Date().toISOString()
    };

    res.write('event: clock\n');
    res.write(`data: ${JSON.stringify(payload)}\n\n`);
  }, 3000);

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

Os cabeçalhos informam que a resposta é um fluxo de eventos e que não deve ser armazenada em cache. flushHeaders() envia os cabeçalhos imediatamente, sem aguardar o primeiro bloco maior de dados. O intervalo escreve um novo evento a cada três segundos.

O evento close é essencial. Quando o usuário fecha a página, perde a conexão ou navega para outra rota, o servidor deve interromper timers e liberar recursos. Sem essa limpeza, a aplicação acumula intervalos, listeners e referências desnecessárias.

Consumindo eventos no navegador

No front-end, use EventSource:

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

source.addEventListener('connected', event => {
  console.log('Conexão aberta', JSON.parse(event.data));
});

source.addEventListener('clock', event => {
  const payload = JSON.parse(event.data);
  document.querySelector('#time').textContent = payload.time;
});

source.onerror = error => {
  console.error('Falha ou reconexão em andamento', error);
};

O navegador tenta reconectar automaticamente quando a conexão cai. Esse comportamento é uma das maiores vantagens do SSE. Ainda assim, a aplicação deve tratar mensagens repetidas e considerar que o cliente pode ficar desconectado por alguns segundos.

Enviando eventos para vários clientes

Em uma aplicação real, os eventos surgem quando um pedido muda, uma tarefa termina ou um serviço externo envia uma atualização. Uma abordagem simples é manter uma coleção de conexões ativas:

const clients = new Map();
let nextClientId = 1;

app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  res.flushHeaders();

  const clientId = nextClientId++;
  clients.set(clientId, res);

  res.write(`event: connected\ndata: {"clientId":${clientId}}\n\n`);

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

function broadcast(eventName, data) {
  const message = [
    `event: ${eventName}`,
    `data: ${JSON.stringify(data)}`,
    '',
    ''
  ].join('\n');

  for (const response of clients.values()) {
    response.write(message);
  }
}

Uma rota comum pode chamar broadcast() depois de atualizar um pedido, finalizar um relatório ou concluir uma tarefa. Esse exemplo mantém as conexões apenas na memória do processo. Ele funciona para desenvolvimento e aplicações pequenas, mas precisa de outra estratégia quando existem múltiplos servidores.

Heartbeats e conexões ociosas

Proxies, balanceadores e redes corporativas podem encerrar conexões que permanecem muito tempo sem transmitir dados. Para evitar isso, envie comentários periódicos. Linhas iniciadas por dois-pontos são ignoradas pelo navegador, mas mantêm tráfego na conexão:

const heartbeat = setInterval(() => {
  res.write(`: heartbeat ${Date.now()}\n\n`);
}, 15000);

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

O intervalo deve ser menor que o tempo limite da infraestrutura. Não use um valor agressivo sem necessidade, pois milhares de clientes podem gerar tráfego e processamento relevantes.

Reconexão e Last-Event-ID

Quando o servidor envia um campo id, o navegador guarda o valor mais recente. Ao reconectar, ele pode incluir o cabeçalho Last-Event-ID. O servidor pode usar esse identificador para reenviar eventos perdidos.

app.get('/events', (req, res) => {
  const lastEventId = req.get('Last-Event-ID');

  if (lastEventId) {
    console.log('Cliente voltou após o evento', lastEventId);
  }

  // configurar cabeçalhos e iniciar o stream
});

Para oferecer recuperação real, mantenha um histórico temporário em banco, Redis ou outro armazenamento. Ao reconectar, busque eventos posteriores ao identificador informado. Defina limites de retenção para impedir que esse histórico cresça indefinidamente.

Autenticação no EventSource

A API nativa EventSource não permite definir cabeçalhos personalizados como Authorization da mesma forma que fetch. Em aplicações do mesmo domínio, cookies de sessão com HttpOnly, Secure e uma política adequada de SameSite costumam ser a opção mais simples.

Outra possibilidade é usar um token curto e específico para a conexão, mas ele não deve ser permanente nem aparecer em logs. Evite colocar tokens de acesso duradouros diretamente na URL. Sempre valide se o usuário possui permissão para receber cada tipo de evento.

Não confunda conexão aberta com autorização contínua. Se a permissão do usuário for revogada, a aplicação precisa encerrar o stream ou impedir novas mensagens. O artigo sobre segurança em aplicações web apresenta cuidados complementares.

CORS e credenciais

Quando front-end e API estão em origens diferentes, configure CORS de maneira explícita. Não combine credenciais com uma origem curinga. No navegador, use:

const source = new EventSource(
  'https://api.exemplo.com/events',
  { withCredentials: true }
);

No servidor, autorize apenas origens conhecidas e envie os cabeçalhos correspondentes. Teste o comportamento em produção, porque cookies, HTTPS, domínio e política SameSite influenciam o envio de credenciais.

Escalando com Redis Pub/Sub

Se a aplicação roda em várias instâncias, cada processo conhece apenas suas próprias conexões. Uma atualização recebida pelo servidor A não chega automaticamente aos clientes conectados ao servidor B. Uma solução comum é publicar eventos em um canal compartilhado e fazer todas as instâncias assinarem esse canal.

Com Redis Pub/Sub, a instância que processa a atualização publica a mensagem. Cada servidor recebe o evento e o transmite aos clientes conectados localmente. O guia sobre Redis com Node.js ajuda a preparar a conexão e entender estruturas de comunicação.

// produtor
await publisher.publish(
  'order-events',
  JSON.stringify(update)
);

// em cada instância
await subscriber.subscribe('order-events', message => {
  broadcast('order-updated', JSON.parse(message));
});

Pub/Sub entrega apenas mensagens enquanto o assinante está conectado. Quando o projeto exige recuperação, auditoria ou processamento garantido, use uma estrutura persistente, como streams, filas ou uma tabela de eventos.

Proxies e buffering

Alguns proxies aguardam acumular dados antes de encaminhar a resposta. Isso destrói a experiência em tempo real, pois vários eventos chegam juntos depois de uma espera. O cabeçalho X-Accel-Buffering: no ajuda em determinadas configurações do Nginx, mas a infraestrutura também precisa ser ajustada corretamente.

Confira tempos limite do balanceador, buffering, compressão, cache e quantidade máxima de conexões. A documentação de HTTP do Node.js é uma referência importante para entender respostas mantidas abertas e eventos de encerramento.

Testando com curl

Você não precisa do navegador para testar o endpoint. Use:

curl -N http://localhost:3000/events

A opção -N desativa o buffering do próprio curl e mostra cada bloco assim que chega. Durante o teste, confirme:

  • o tipo de conteúdo text/event-stream;
  • a chegada gradual dos eventos;
  • a separação por duas quebras de linha;
  • o encerramento dos timers após desconectar;
  • a reconexão automática no navegador;
  • a autorização por usuário;
  • o comportamento atrás do proxy real.

Erros comuns

  • Esquecer a linha em branco: o navegador não considera o evento completo.
  • Não limpar recursos: timers e listeners continuam ativos após a desconexão.
  • Guardar todas as conexões sem limite: a aplicação pode esgotar memória e descritores.
  • Ignorar o proxy: buffering e timeouts impedem atualizações imediatas.
  • Usar token permanente na URL: credenciais podem aparecer em logs e histórico.
  • Confiar apenas na memória: múltiplas instâncias não compartilham clientes nem eventos.
  • Não criar heartbeat: conexões ociosas podem ser encerradas pela infraestrutura.
  • Enviar dados sem filtrar: um usuário pode receber eventos de outro usuário.

Boas práticas para produção

  • Envie eventos pequenos e objetivos.
  • Inclua identificadores para permitir recuperação.
  • Use heartbeats com intervalo compatível com a infraestrutura.
  • Monitore conexões abertas, duração, erros e bytes transmitidos.
  • Defina limites por usuário e por instância.
  • Valide autorização antes de cada transmissão sensível.
  • Use armazenamento compartilhado ao escalar horizontalmente.
  • Teste reinicialização, perda de rede e reconexão.
  • Documente quais eventos existem e o formato de cada payload.

Conclusão

Server-Sent Events com Node.js é uma solução eficiente quando o servidor precisa enviar atualizações contínuas ao navegador sem comunicação bidirecional permanente. A integração com EventSource, a reconexão automática e o uso do HTTP tornam a implementação mais simples do que muitas alternativas para notificações, progresso e dashboards.

Comece com um endpoint pequeno, trate desconexões e adicione heartbeats. Depois, implemente identificadores, autenticação e recuperação de eventos. Quando a aplicação crescer, distribua mensagens por Redis ou outra infraestrutura compartilhada e revise cuidadosamente proxies e limites de conexão. Com esses cuidados, SSE pode oferecer tempo real de maneira previsível e fácil de manter.

Os 10 Melhores Cursos de Programação de 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