Server-Sent Events (SSE) Development for Web Application
Server-Sent Events (SSE) — technology for sending events from server to client over HTTP connection. One-directional: server → client. Simpler than WebSocket for tasks where bidirectional communication not needed: notifications, long operation progress, real-time feeds.
SSE Advantages over WebSocket
- HTTP/1.1 compatible — SSE works through plain HTTP, no upgrade needed
-
Automatic reconnection — browser automatically reconnects on break (via
retryfield) - Standard headers — Authorization, cookies work without hassles
- Proxy-friendly — plain HTTP, fewer corporate proxy problems
- No CORS preflight for GET requests
SSE Format
Server response — text/event-stream with data:, event:, id:, retry: fields:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"type":"notification","message":"New order"}
event: order_update
data: {"orderId":"123","status":"shipped"}
id: msg_456
retry: 3000
: comment (ignored by client)
Server Implementation (Node.js + Express)
app.get('/api/events', (req, res) => {
const userId = req.user.id;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // for Nginx: disable buffering
});
// Send first packet immediately (bypass nginx buffering)
res.write(':ok\n\n');
// Add client to registry
const clientId = nanoid();
clients.set(clientId, { res, userId });
// Heartbeat every 15 seconds
const heartbeat = setInterval(() => {
res.write(': ping\n\n');
}, 15000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(clientId);
});
});
// Send event to specific user
function sendToUser(userId: string, event: string, data: object) {
clients.forEach(({ res, userId: uid }) => {
if (uid === userId) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
});
}
// Broadcast to all
function broadcast(event: string, data: object) {
clients.forEach(({ res }) => {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
}
Client (Browser)
const eventSource = new EventSource('/api/events', { withCredentials: true });
// Default events (event: without name)
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log('Message:', data);
};
// Named events
eventSource.addEventListener('order_update', (e) => {
const order = JSON.parse(e.data);
updateOrderStatus(order.orderId, order.status);
});
eventSource.addEventListener('notification', (e) => {
showNotification(JSON.parse(e.data).message);
});
// Error handling
eventSource.onerror = (e) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed, auto-reconnecting...');
}
};
Scaling with Redis Pub/Sub
Multiple servers — Redis Pub/Sub for distributed event delivery (like WebSocket):
sub.subscribe('user:events', (message) => {
const { userId, event, data } = JSON.parse(message);
sendToUser(userId, event, data);
});
// From any service
await pub.publish('user:events', JSON.stringify({
userId: 'user_123',
event: 'payment_completed',
data: { amount: 5000 }
}));
SSE Limitations
- Server → client only — client cannot send data through SSE (only new EventSource requests or separate API request)
- Connection limit in HTTP/1.1 — browser limits 6 connections per domain. SSE takes one. Solution: HTTP/2 (single multiplexed stream).
-
IE unsupported — polyfill
eventsourcefor older browsers
Practical Use Cases
- Progress long operations (file import, report generation)
- Real-time notifications in user account
- Counter updates (new messages, orders)
- Live news feed or sports results
Timeline
SSE endpoint with authentication, heartbeat, Redis scaling: 3–5 days. With typed events, client hook (useSSE), notification integration: 1–2 weeks.







