SSE with Express.js — Complete Guide
In this tutorial, you will learn about SSE with Express.js"Express" >}}.js. We cover key concepts, practical examples, and best practices to help you master this topic.
Implement Server-Sent Events in Express.js: create GET routes, set SSE headers, stream events, handle client disconnection, and broadcast events to multiple connected clients.
What You Learn
You will learn how to implement SSE endpoints in Express.js, properly set headers, stream events to clients, detect client disconnection, and broadcast events to all connected clients.
Why It Matters
Express.js is one of the most popular Node.js frameworks. Adding SSE support enables real-time features without additional libraries. Understanding the Express SSE pattern allows integration into existing Express applications.
Real-World Use
DodaTech's Express-based API server uses SSE for deployment status updates. When a developer deploys code, the server pushes deployment logs to the browser. Multiple team members can watch the same deployment in real time.
Basic SSE Endpoint
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 counter = 0;
const interval = setInterval(() => {
counter++;
const data = JSON.stringify({ count: counter, time: new Date() });
res.write(`id: ${counter}\n`);
res.write(`data: ${data}\n\n`);
if (counter >= 10) {
clearInterval(interval);
res.end();
}
}, 1000);
req.on('close', () => {
clearInterval(interval);
console.log('Client disconnected');
});
});
app.listen(3000, () => console.log('SSE on http://localhost:3000/events'));
Client Disconnection Handling
const express = require('express');
const app = express();
app.get('/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendEvent = (type, data) => {
res.write(`event: ${type}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
sendEvent('connected', { message: 'Stream established' });
// Monitor for client disconnect
let isConnected = true;
req.on('close', () => {
isConnected = false;
console.log('Client left, cleaning up');
cleanup();
});
// Heartbeat to detect dropped connections
const heartbeat = setInterval(() => {
if (!isConnected) {
clearInterval(heartbeat);
return;
}
res.write(': heartbeat\n\n');
}, 15000);
// Simulate events
const eventSource = setInterval(() => {
if (!isConnected) {
clearInterval(eventSource);
return;
}
sendEvent('update', { value: Math.random(), timestamp: Date.now() });
}, 2000);
function cleanup() {
clearInterval(heartbeat);
clearInterval(eventSource);
}
});
app.listen(3000);
Broadcasting to Multiple Clients
const express = require('express');
const app = express();
class SSEClientManager {
constructor() {
this.clients = new Map();
this.clientId = 0;
}
addClient(res) {
this.clientId++;
const id = this.clientId;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
res.write(`data: ${JSON.stringify({ clientId: id })}\n\n`);
this.clients.set(id, res);
req.on('close', () => {
this.clients.delete(id);
console.log(`Client ${id} disconnected (${this.clients.size} remaining)`);
});
console.log(`Client ${id} connected (${this.clients.size} total)`);
return id;
}
broadcast(event, data) {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const [id, client] of this.clients) {
client.write(message);
}
}
sendTo(clientId, event, data) {
const client = this.clients.get(clientId);
if (client) {
client.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
}
}
getCount() {
return this.clients.size;
}
}
const manager = new SSEClientManager();
app.get('/events', (req, res) => {
manager.addClient(res);
});
app.post('/broadcast', express.json(), (req, res) => {
const { event, data } = req.body;
manager.broadcast(event || 'message', data);
res.json({ sent: true, clients: manager.getCount() });
});
// Simulate periodic broadcasts
setInterval(() => {
manager.broadcast('heartbeat', { time: Date.now(), clients: manager.getCount() });
}, 5000);
app.listen(3000, () => console.log('SSE broadcast server on :3000'));
Event Types and Named Streams
const express = require('express');
const app = express();
const streams = {
notifications: [],
metrics: [],
logs: [],
};
function subscribe(stream, res) {
if (!streams[stream]) streams[stream] = [];
streams[stream].push(res);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
});
res.write(`event: subscribed\ndata: ${JSON.stringify({ stream })}\n\n`);
req.on('close', () => {
streams[stream] = streams[stream].filter(s => s !== res);
});
}
app.get('/stream/:type', (req, res) => {
const { type } = req.params;
if (!streams[type]) {
res.status(404).json({ error: 'Unknown stream' });
return;
}
subscribe(type, res);
});
app.post('/publish/:type', express.json(), (req, res) => {
const { type } = req.params;
const { event, data } = req.body;
const subscribers = streams[type] || [];
subscribers.forEach(client => {
client.write(`event: ${event || 'message'}\ndata: ${JSON.stringify(data)}\n\n`);
});
res.json({ published: true, subscribers: subscribers.length });
});
app.get('/streams', (req, res) => {
const status = {};
for (const [name, subs] of Object.entries(streams)) {
status[name] = subs.length;
}
res.json(status);
});
app.listen(3000);
SSE with Express Router
const express = require('express');
const router = express.Router();
// Middleware for SSE endpoints
function sseMiddleware(req, res, next) {
res.sse = {
send(event, data) {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
},
sendMessage(data) {
res.write(`data: ${JSON.stringify(data)}\n\n`);
},
};
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
req.on('close', () => {
console.log('SSE client disconnected');
res.sse = null;
});
next();
}
router.get('/notifications', sseMiddleware, (req, res) => {
res.sse.send('connected', { message: 'Notification stream ready' });
const interval = setInterval(() => {
res.sse.send('notification', {
title: 'New update',
time: Date.now(),
});
}, 3000);
req.on('close', () => clearInterval(interval));
});
router.get('/metrics', sseMiddleware, (req, res) => {
res.sse.sendMessage({ status: 'metrics stream ready' });
});
module.exports = router;
// app.use('/sse', router);
Common Mistakes
1. Missing Headers
Without Content-Type: text/event-stream, the response is buffered. Without Cache-Control: no-cache, proxies cache the stream.
2. Not Handling Client Disconnect
Without req.on('close'), resources leak when clients disconnect. Always clean up intervals and references.
3. Using res.send() Instead of res.write()
res.send() ends the response. Use res.write() for streaming. res.end() only when the stream is complete.
4. Blocking the Event Loop
Synchronous operations in the SSE handler block all clients. Use asynchronous patterns for event generation.
5. No Heartbeat
Without periodic data or comments, some proxies and load balancers close idle connections. Send a comment (: ...) every 15-30 seconds.
Practice Questions
1. Why use res.write() instead of res.send() in SSE endpoints?
res.write() keeps the connection open for multiple messages. res.send() ends the response after the first message.
2. How do you detect client disconnection in Express?
Listen for the req.on('close') event. Clean up intervals, timers, and client references when triggered.
3. How do you broadcast events to multiple clients?
Maintain a list of response objects (res) for each connected client. Iterate over the list and call res.write() on each.
4. What is the purpose of SSE heartbeat comments?
Keep-alive comments (lines starting with :) prevent proxies and load balancers from closing idle connections.
Challenge
Build an Express SSE server that: supports multiple named streams (alerts, metrics, logs), broadcasts events from POST /publish/:stream, handles client disconnection for each stream, sends heartbeat every 15 seconds, and provides GET /streams status endpoint.
FAQ
Mini Project: Express SSE Dashboard
const express = require('express');
const app = express();
const clients = [];
app.get('/dashboard/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no',
});
clients.push(res);
res.write(`event: connected\ndata: {"clients": ${clients.length}}\n\n`);
req.on('close', () => {
const idx = clients.indexOf(res);
if (idx > -1) clients.splice(idx, 1);
});
});
function broadcast(event, data) {
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
clients.forEach(c => c.write(msg));
}
// Simulate dashboard metrics
setInterval(() => {
broadcast('metric', {
cpu: Math.random() * 100,
memory: Math.random() * 100,
requests: Math.floor(Math.random() * 1000),
time: Date.now(),
});
}, 2000);
setInterval(() => {
broadcast('client-count', { count: clients.length });
}, 5000);
app.listen(3000, () => console.log('Dashboard SSE on :3000/dashboard/stream'));
What's Next
Now that you understand SSE with Express, learn SSE with Django, then explore SSE with FastAPI.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro