SSE with Node.js HTTP — Complete Guide
In this tutorial, you will learn about SSE with Node.js HTTP. We cover key concepts, practical examples, and best practices to help you master this topic.
Implement Server-Sent Events with raw Node.js HTTP: create SSE servers, set headers, stream events, handle client disconnection, manage multiple connections, and broadcast events.
What You Learn
You will learn how to implement SSE endpoints using Node.js built-in http module, manage connected clients, handle disconnection, broadcast events, and build a reusable SSE server class.
Why It Matters
Understanding SSE at the raw HTTP level reveals how it works under the hood. The http module approach works without frameworks, making it portable across any Node.js application and useful for understanding Express SSE internals.
Real-World Use
DodaTech's lightweight status server uses raw Node.js HTTP for SSE. The server pushes health check results to monitoring tools. No frameworks needed, just 50 lines of code for a production SSE endpoint.
Basic SSE Server
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let counter = 0;
const interval = setInterval(() => {
counter++;
const data = JSON.stringify({
count: counter,
timestamp: Date.now(),
});
res.write(`id: ${counter}\ndata: ${data}\n\n`);
if (counter >= 10) {
clearInterval(interval);
res.end();
}
}, 1000);
req.on('close', () => {
clearInterval(interval);
console.log('Client disconnected');
});
} else {
res.writeHead(404);
res.end();
}
});
server.listen(3000, () => {
console.log('SSE server on http://localhost:3000/events');
});
Client Manager Class
const http = require('http');
class SSEServer {
constructor() {
this.clients = new Set();
this.clientId = 0;
}
handleConnection(req, res) {
if (req.url !== '/events') {
res.writeHead(404);
res.end();
return;
}
this.clientId++;
const clientInfo = {
id: this.clientId,
res,
connectedAt: Date.now(),
userAgent: req.headers['user-agent'],
};
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
res.write(`event: connected\ndata: ${JSON.stringify({
clientId: clientInfo.id,
message: 'SSE connection established'
})}\n\n`);
this.clients.add(clientInfo);
console.log(`Client ${clientInfo.id} connected (${this.clients.size} total)`);
req.on('close', () => {
this.clients.delete(clientInfo);
console.log(`Client ${clientInfo.id} disconnected (${this.clients.size} remaining)`);
});
}
broadcast(event, data) {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of this.clients) {
client.res.write(message);
}
}
sendTo(clientId, event, data) {
for (const client of this.clients) {
if (client.id === clientId) {
client.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
return true;
}
}
return false;
}
getClientCount() {
return this.clients.size;
}
getClients() {
return Array.from(this.clients).map(c => ({
id: c.id,
connectedAt: c.connectedAt,
userAgent: c.userAgent,
}));
}
}
const sseServer = new SSEServer();
const server = http.createServer((req, res) => {
if (req.url === '/events') {
sseServer.handleConnection(req, res);
} else if (req.url === '/status' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
clients: sseServer.getClientCount(),
clientList: sseServer.getClients(),
}));
} else {
res.writeHead(404);
res.end();
}
});
setInterval(() => {
sseServer.broadcast('heartbeat', {
time: Date.now(),
clients: sseServer.getClientCount(),
});
}, 5000);
server.listen(3000);
Keep-Alive and Reconnection
const http = require('http');
const clients = new Set();
const server = http.createServer((req, res) => {
if (req.url !== '/stream') {
res.writeHead(404);
res.end();
return;
}
const lastEventId = req.headers['last-event-id'];
console.log(`Client connecting, last event: ${lastEventId || 'none'}`);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// Send retry interval (5 seconds)
res.write('retry: 5000\n\n');
if (lastEventId) {
res.write(`event: resuming\ndata: ${JSON.stringify({ fromId: lastEventId })}\n\n`);
}
clients.add(res);
// Heartbeat every 15 seconds
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 15000);
req.on('close', () => {
clients.delete(res);
clearInterval(heartbeat);
});
// Send events
let eventId = parseInt(lastEventId) || 0;
const stream = setInterval(() => {
eventId++;
const data = JSON.stringify({
id: eventId,
time: Date.now(),
});
res.write(`id: ${eventId}\ndata: ${data}\n\n`);
}, 2000);
req.on('close', () => {
clearInterval(stream);
});
});
server.listen(3000);
Event History and Replay
const http = require('http');
class SSEEventStore {
constructor(maxEvents = 100) {
this.events = [];
this.maxEvents = maxEvents;
this.counter = 0;
}
addEvent(event, data) {
this.counter++;
const entry = {
id: this.counter,
event,
data,
timestamp: Date.now(),
};
this.events.push(entry);
if (this.events.length > this.maxEvents) {
this.events.shift();
}
return entry;
}
getEventsFrom(lastId) {
const startId = parseInt(lastId) || 0;
return this.events.filter(e => e.id > startId);
}
}
const eventStore = new SSEEventStore();
const clients = new Set();
const server = http.createServer((req, res) => {
if (req.url === '/stream') {
const lastEventId = req.headers['last-event-id'];
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
});
// Replay missed events
if (lastEventId) {
const missedEvents = eventStore.getEventsFrom(lastEventId);
for (const event of missedEvents) {
res.write(`id: ${event.id}\nevent: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
}
}
clients.add(res);
req.on('close', () => {
clients.delete(res);
});
} else if (req.url === '/publish' && req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const { event, data } = JSON.parse(body);
const entry = eventStore.addEvent(event, data);
const message = `id: ${entry.id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of clients) {
client.write(message);
}
res.writeHead(200);
res.end(JSON.stringify({ id: entry.id, sent: clients.size }));
});
} else {
res.writeHead(404);
res.end();
}
});
server.listen(3000);
Common Mistakes
1. Not Calling res.end()
SSE connections should stay open. Only call res.end() when intentionally closing the stream. For long-lived streams, never call res.end().
2. Missing Keep-Alive
Without periodic data or comments, proxies time out idle connections. Send a comment line (: ...) every 15-30 seconds.
3. Not Handling Backpressure
If the client is slow, res.write() buffers internally. For high-throughput streams, monitor res.destroyed and res.writableEnded.
4. No Error Handling on res.write()
If the client disconnects, res.write() throws an error. Wrap in try/catch or check res.destroyed before writing.
5. Memory Leaks from Abandoned Clients
Clients that disconnect without close event leave stale references. Always clean up in req.on('close').
Practice Questions
1. How does raw Node.js SSE detect client disconnection?
Listen for the req.on('close') event. This fires when the client disconnects or the socket times out.
2. What is the purpose of the 'retry' field in SSE?
It tells the browser how many milliseconds to wait before reconnecting after disconnection. Set it early in the stream.
3. How do you broadcast events to all connected clients?
Maintain a Set of response objects. Iterate over the set and call res.write() on each. Remove clients on close.
4. How do you replay missed events on reconnection?
Track the Last-Event-ID from the request headers. Query an event store for events with IDs greater than that value and send them.
Challenge
Build a raw Node.js SSE server with: event history (last 50 events stored in memory), replay on reconnection using Last-Event-ID, broadcast via POST /publish endpoint, client listing at GET /clients, heartbeat every 10 seconds, and a maximum of 1000 concurrent clients.
FAQ
Mini Project: Node.js SSE Notifications
const http = require('http');
const clients = new Map();
let clientId = 0;
const server = http.createServer((req, res) => {
if (req.url === '/notifications') {
clientId++;
const id = clientId;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
res.write(`event: connected\ndata: ${JSON.stringify({ clientId: id })}\n\n`);
clients.set(id, res);
broadcast('clients', { count: clients.size });
req.on('close', () => {
clients.delete(id);
broadcast('clients', { count: clients.size });
});
} else {
res.writeHead(404);
res.end();
}
});
function broadcast(event, data) {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const [id, client] of clients) {
try {
client.write(message);
} catch (e) {
clients.delete(id);
}
}
}
// Simulate notifications
setInterval(() => {
broadcast('notification', {
id: Date.now(),
title: 'New Event',
message: `Event at ${new Date().toLocaleTimeString()}`,
});
}, 5000);
server.listen(3000, () => {
console.log('Notification SSE on :3000/notifications');
});
What's Next
Now that you understand SSE with raw Node.js, explore event types and custom events, then learn about auto-reconnection patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro