As of March 2026, Server-Sent Events (SSE) are experiencing a renaissance. While WebSockets got all the attention for years, SSE is making a comeback—simpler, works over standard HTTP/2, automatic reconnection, and perfect for server-to-client streaming.
Here are 5 patterns you need to know:
1. Basic SSE Endpoint
The simplest pattern—just stream events to the client:
app.get('/api/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: new Date() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
});
Client-side:
const evtSource = new EventSource('/api/events/stream');
evtSource.onmessage = (e) => console.log(JSON.parse(e.data));
2. Named Events for Message Routing
Send specific event types clients can listen for:
// Server
function sendNotification(res, type, payload) {
res.write(`event: ${type}\n`);
res.write(`data: ${JSON.stringify(payload)}\n\n`);
}
sendNotification(res, 'user_joined', { user: 'alice' });
sendNotification(res, 'price_update', { price: 42.50 });
// Client
const source = new EventSource('/api/stream');
source.addEventListener('price_update', (e) => {
const data = JSON.parse(e.data);
updatePriceDisplay(data.price);
});
3. Automatic Reconnection with Last-Event-ID
Never lose sync—track where you left off:
// Server: respect Last-Event-ID header
app.get('/api/logs/stream', (req, res) => {
const lastId = req.headers['last-event-id'];
let cursor = lastId ? parseInt(lastId) : 0;
const stream = db.logs.find({ id: { $gt: cursor } }).cursor();
stream.on('data', (log) => {
res.write(`id: ${log.id}\n`);
res.write(`data: ${JSON.stringify(log)}\n\n`);
});
});
The browser automatically reconnects and sends Last-Event-ID. Critical for production!
4. Heartbeat to Prevent Timeout
Keep connections alive over proxies and load balancers:
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n'); // comment = ignored by client
}, 15000); // every 15 seconds
req.on('close', () => clearInterval(heartbeat));
});
The : prefix makes it a comment—client ignores it but the connection stays warm.
5. Graceful Degradation with Retry
Control reconnection behavior:
// Server: tell client to retry after 5 seconds if disconnected
res.write(`retry: 5000\n\n`);
// Or dynamically based on load
const retryMs = load > 80 ? 10000 : 2000;
res.write(`retry: ${retryMs}\n\n`);
When to Use SSE vs WebSockets
| Scenario | Use SSE | Use WebSockets |
|---|---|---|
| Server → Client only | ✅ | ✅ |
| Bidirectional | ❌ | ✅ |
| HTTP/2 multiplexing | ✅ | ⚠️ |
| Binary data | ❌ | ✅ |
| Simple real-time updates | ✅ | ❌ |
| Firewall-restricted networks | ✅ | ❌ |
Bottom line: For dashboards, notifications, live feeds—SSE wins. For games, collaborative editing, complex bi-directional sync—WebSockets.
Quick Tip: CORS for SSE
If your API and frontend are on different domains:
app.get('/api/stream', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://your-app.com');
res.setHeader('Access-Control-Expose-Headers', 'Last-Event-ID');
// ... rest of SSE setup
});
That's it! SSE is underutilized in 2026—give your users a lighter real-time experience.
Top comments (0)