Criando aplicações em tempo real com SSE (Server-Sent Events)
Há cerca de uma década, se precisássemos desenvolver uma aplicação em tempo real, como um chat ou um feed de atualizações, a primeira solução que viria à mente seria o uso de polling. Essa técnica consiste em enviar requisições HTTP de forma temporizada e recorrente ao servidor, buscando dados atualizados. No entanto, mesmo quando não há novas informações disponíveis, essas requisições continuam sendo disparadas, resultando em desperdício de recursos, como largura de banda e processamento do servidor.

Felizmente, os tempos mudaram. Hoje, ao trabalharmos com JavaScript, contamos com a biblioteca EventSource (opens in a new window), que permite estabelecer uma conexão SSE (Server-Sent Events). Neste artigo, explorarei os conceitos por trás desse recurso e apresentarei um breve tutorial para aplicar esses conceitos na prática.
Diferentemente das requisições HTTP convencionais, onde o cliente dispara uma requisição e o servidor devolve uma resposta, as conexões SSE permanecem sempre abertas. Isso possibilita uma comunicação unidirecional entre servidor e cliente. Ao contrário dos WebSockets, que permitem comunicação bidirecional, no SSE apenas o servidor envia dados ao cliente, que os recebe de forma instantânea enquanto está conectado.

Uma vez estabelecida a conexão, os dados chegam ao cliente JavaScript na forma de eventos, eliminando a necessidade de disparar novas requisições ao servidor para buscar atualizações, como acontece no polling. Cabe ao servidor a responsabilidade de enviar eventos com os dados atualizados sempre que necessário.

A menos que a conexão seja encerrada, o cliente fica constantemente aguardando novos eventos de dados do servidor, tornando essa técnica ideal para a construção de notificações, dashboards ou chats que necessitam de atualizações constantes.
Para demonstrar na prática os conceitos abordados, vamos criar uma aplicação em Node.js (back-end) responsável por disponibilizar um endpoint de conexão SSE, que será utilizado por um cliente JavaScript (front-end).
Nossa aplicação consiste em um sistema onde o usuário deve informar um UserID e estabelecer uma conexão SSE. A partir disso, o servidor começa a disparar, a cada 5 segundos, um evento que retorna a lista de usuários conectados. Caso o usuário encerre sua conexão, ele é removido automaticamente da lista de usuários conectados.
Vamos começar!
npm init -y
npm i express
Ao criar o arquivo index.js
, vamos centralizar a lógica do nosso back-end. Além disso vamos criar um pasta /public
onde ficarão o index.html
e arquivo script.js
para gerenciar nossa página. A estrutura deve ficar assim:
/public index.html script.jsindex.jspackage.json
Em index.js
, vamos importar a biblioteca express
, que é responsável por permitir a criação de endpoints HTTP:
import express from "express";
const app = express();app.use(express.json());
const PORT = 3000;app.listen(PORT, () => console.log(`Server is running on ${PORT} port`));
Além disso, é necessário configurar para que o conteúdo da pasta /public
seja retornado quando o usuário acessar http://localhost:3000/
:
import express from "express";import fs from "fs";import path from "path";
const app = express();app.use(express.json());app.use(express.static(path.join(path.resolve(path.dirname("")), "public")));
app.get("/", (_, res) => { res.writeHead(200, { "content-language": "text/html" }); const streamIndexHtml = fs.createReadStream("index.html"); streamIndexHtml.pipe(res);});
const PORT = 3000;app.listen(PORT, () => console.log(`Server is running on ${PORT} port`));
Dentro de /public
em index.html
vamos montar um HTML básico para retornar um título e testar o funcionando do servidor:
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <title>Server Sent Events Demo</title> </head> <body> <h1>Server Sent Events Demo (SSE)</h1> </body></html>
Agora, ao rodar o comando npm start
no terminal, a página já deve ser exibida no navegador:

Voltando as atenções para o back-end, podemos criar arquivo connection.js
, responsável por gerenciar as conexões do usuários ao servidor:
/public index.html script.jsindex.jsconnection.jspackage.json
Dentro dele, vamos exportar uma classe Connection
, onde será armazenado um mapa de usuários conectados, além dos métodos registerUser
, para registrar novos usuários, e removeUser
, para removê-los do mapa:
export default class Connection { _users = new Map();
registerUser() {}
removeUser() {}}
Também podemos criar um getter connectedUsers
para facilitar o acesso aos usuários conectados fora da classe:
export default class Connection { _users = new Map();
get connectedUsers() { return [...this._users.keys()]; }
registerUser() {}
removeUser() {}}
No método registerUser, vamos receber o userId
do usuário conectado e gerar um connectionId
, que servirá como identificador único para essa conexão estabelecida:
registerUser(userId, response) { if (!this._users.has(userId)) { this._users.set(userId, []) }
const connectionId = this.generateConnectionId() this._users.get(userId).push({ connectionId, response }) return connectionId}
No método generateConnectionId
, podemos utilizar a biblioteca crypto
para gerar e retornar um token aleatório:
generateConnectionId() { return crypto.randomBytes(20).toString("hex")}
Já o método removeUser
receberá o userId
e o connectionId
do usuário que fechou a conexão, e, com isso, removerá o usuário do mapa:
removeUser(userId, connectionId) { if (!this._users.has(userId)) { return }
const connections = this._users .get(userId) .filter((connection) => connection.connectionId !== connectionId)
if (connections.length) { this._users.set(userId, connections) } else { this._users.delete(userId) }}
Como a classe Connection
isola a lógica de gerenciamento das conexões, precisamos configurar um endpoint em nosso servidor (index.js
) para que os clientes possam iniciar uma comunicação SSE. Essas conexões, por sua vez, serão gerenciadas pela classe Connection
:
const connection = new Connection();
app.get("/events", (req, res) => { res .writeHead(200, { "Cache-Control": "no-cache", "Content-Type": "text/event-stream", Connection: "keep-alive", }) .write("\n");
const EVENTS_INTERVAL = 5000; const userId = req.query.user; const connectionId = connection.registerUser(userId, res);
setInterval(() => { res.write(`data: ${JSON.stringify(connection.connectedUsers)}\n\n`); }, EVENTS_INTERVAL);
req.on("close", () => connection.removeUser(userId, connectionId));});
Aqui podemos destacar três pontos importantes:
-
O endpoint
/events
deve responder ao cliente com o cabeçalhoContent-Type: text/event-stream
. Isso permite que os clientes reconheçam a comunicação SSE e criem um objetoEventSource
para estabelecer a conexão. -
A cada cinco segundos, o servidor enviará uma resposta em forma de evento para os clientes conectados, informando quais usuários estabeleceram conexão.
-
Conexões encerradas podem ser monitoradas utilizando o evento
req.on("close", () => {})
, permitindo que usuários desconectados sejam removidos do mapa de conexões.
Nosso back-end está pronto! Agora podemos voltar nossa atenção para o front-end.
Como o servidor já disponibiliza um endpoint para conexões SSE, cabe ao cliente requisitar esse endereço e aguardar o recebimento dos eventos enviados.
No arquivo index.html
, vamos criar:
- Um campo de texto para o usuário informar seu UserID;
- Dois botões: um para iniciar e outro para encerrar a conexão SSE;
- Uma tag
<ul>
para exibir a lista de usuários conectados, que será atualizada com os eventos enviados pelo servidor.
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Server Sent Events Demo</title> </head> <body> <h1>Server Sent Events Demo (SSE)</h1>
<input type="text" id="userIdTxt" placeholder="User ID" /> <input type="button" id="connectBtn" value="Connect" /> <input type="button" id="closeBtn" value="Close" disabled /> <ul id="users"></ul> <script type="text/javascript" src="script.js"></script> </body></html>
Por fim, no arquivo script.js
para isolar a lógica de manipulação dos componentes e a criação do objeto EventSource
.
let eventSource = null;const userIdInput = document.querySelector("#userIdTxt");const connectButton = document.querySelector("#connectBtn");const closeButton = document.querySelector("#closeBtn");const ulUsers = document.querySelector("#users");
connectButton.addEventListener("click", startConnection);closeButton.addEventListener("click", closeConnection);
function startConnection() { const userId = userIdInput.value.trim(); if (!userId) { alert("Please enter a User ID"); return; }
eventSource = new EventSource(`/events?user=${userId}`); connectButton.setAttribute("disabled", ""); closeButton.removeAttribute("disabled"); userIdInput.setAttribute("disabled", "");
eventSource.onmessage = (event) => { const connectedUsers = JSON.parse(event.data); updateUsersList(connectedUsers); };}
function updateUsersList(users) { ulUsers.innerHTML = "";
users.forEach((user) => { const liUser = document.createElement("li"); liUser.textContent = user; ulUsers.appendChild(liUser); });}
function closeConnection() { eventSource.close();
ulUsers.innerHTML = ""; connectButton.removeAttribute("disabled"); userIdInput.removeAttribute("disabled"); closeButton.setAttribute("disabled", "");}
Aqui, destacamos a criação do objeto eventSource
:
eventSource = new EventSource(`/events?user=${userId}`)
.
No construtor, ele espera como argumento um servidor que responda com o conteúdo text/event-stream
. Por sorte, já configuramos exatamente isso! 😜
A função eventSource.onmessage(event => {})
permite receber em tempo real todos os eventos enviados pelo servidor de forma unidirecional.
Para evitar que a conexão permaneça aberta indefinidamente, podemos utilizar eventSource.close()
, que fecha o canal de comunicação com o servidor. Alternativamente, a conexão também será encerrada ao fechar o navegador.
Ao abrir duas abas da nossa aplicação no navegador, podemos validar o funcionamento da conexão em tempo real, observando as atualizações de usuários conectados em ação.

Note que, enquanto a conexão SSE permanecer aberta, os eventos continuarão sendo recebidos a cada 5 segundos, atualizando automaticamente a lista de usuários conectados ao servidor.

Com isso, concluímos a implementação de um sistema simples utilizando SSE para conexões em tempo real. Essa técnica é ideal para casos em que o servidor precisa enviar dados contínuos e unidirecionais para o cliente, como feeds, chats ou dashboards. Se você quiser conferir o código completo deste projeto, acesse meu repositório no GitHub: Server Sent Events (opens in a new window).