Server-Sent Events vs WebSockets vs HTTP/2 Push: When to Use What
Last year I was building a real-time notification system for BirJob. Every time a new job matched a user's saved search, they needed to see it instantly — not on the next page refresh, not after a polling interval, but now. The obvious answer seemed like WebSockets. Real-time equals WebSockets, right?
Wrong. After implementing WebSockets, I realized I was maintaining persistent bidirectional connections for a use case that was purely unidirectional — the server sends notifications, the client never sends anything back except "I'm still here." I ripped out WebSockets and replaced them with Server-Sent Events (SSE). The result: 60% less code, zero connection management complexity, automatic reconnection handled by the browser, and the same real-time experience for users.
The lesson: real-time doesn't automatically mean WebSockets. There are three major technologies for pushing data from server to client, and choosing the wrong one adds complexity without benefit. This guide breaks down when each technology is the right choice — with benchmarks, production examples, and an honest comparison table.
The Three Technologies: A Quick Overview
| Technology | Direction | Protocol | Connection | Browser Support |
|---|---|---|---|---|
| Server-Sent Events | Server → Client only | HTTP/1.1 (long-lived) | Persistent, auto-reconnect | All modern (no IE) |
| WebSockets | Bidirectional | ws:// / wss:// | Persistent, manual reconnect | All modern |
| HTTP/2 Server Push | Server → Client (assets) | HTTP/2 | Multiplexed streams | All modern |
Server-Sent Events: The Underrated Default
SSE is an HTTP-based protocol for streaming events from server to client over a standard HTTP connection. The client opens a connection, and the server keeps it open, sending events as they occur.
Server Implementation (Node.js/Express)
// SSE endpoint
app.get('/api/events', (req, res) => {
// Set SSE headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // Disable Nginx buffering
});
// Send initial connection event
res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: Date.now() })}\n\n`);
// Send heartbeat every 30 seconds (keep connection alive)
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n'); // SSE comment — keeps connection alive
}, 30000);
// Subscribe to events (using Redis pub/sub, event emitter, etc.)
const userId = req.user.id;
const onEvent = (event) => {
res.write(`id: ${event.id}\n`);
res.write(`event: ${event.type}\n`);
res.write(`data: ${JSON.stringify(event.data)}\n`);
res.write(`retry: 5000\n\n`); // Reconnect after 5s on disconnect
};
eventBus.subscribe(userId, onEvent);
// Cleanup on disconnect
req.on('close', () => {
clearInterval(heartbeat);
eventBus.unsubscribe(userId, onEvent);
});
});
Client Implementation
// Browser-native EventSource API
const eventSource = new EventSource('/api/events', {
// withCredentials: true // Include cookies for auth
});
// Handle named events
eventSource.addEventListener('job-match', (event) => {
const data = JSON.parse(event.data);
showNotification(`New job match: ${data.title} at ${data.company}`);
});
eventSource.addEventListener('message-received', (event) => {
const data = JSON.parse(event.data);
updateInbox(data);
});
// Connection lifecycle
eventSource.onopen = () => console.log('SSE connected');
eventSource.onerror = (err) => {
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('SSE reconnecting...'); // Auto-reconnect!
}
};
Why SSE is Underrated
- Automatic reconnection. The browser's
EventSourceAPI automatically reconnects on connection drop. WebSockets require you to implement reconnection logic manually. - Event IDs and replay. SSE supports
idfields. On reconnection, the browser sendsLast-Event-IDheader, allowing the server to replay missed events. WebSockets have no built-in replay mechanism. - Works through HTTP proxies. SSE uses standard HTTP. It works through corporate proxies, CDNs, and load balancers without special configuration. WebSockets require proxy upgrades.
- Simpler infrastructure. SSE endpoints are regular HTTP routes. No special WebSocket servers, no connection upgrade handling, no separate port management.
According to MDN Web Docs, SSE is supported by all modern browsers (Chrome, Firefox, Safari, Edge). The only exception is Internet Explorer, which is no longer supported by Microsoft.
WebSockets: When You Need Bidirectional Communication
WebSockets provide full-duplex, bidirectional communication over a single TCP connection. Both the client and server can send messages at any time — there's no request-response pattern.
// Server (ws library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Map(); // userId -> WebSocket
wss.on('connection', (ws, req) => {
const userId = authenticateFromRequest(req);
clients.set(userId, ws);
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.type) {
case 'chat':
// Broadcast to recipient
const recipient = clients.get(message.recipientId);
if (recipient?.readyState === WebSocket.OPEN) {
recipient.send(JSON.stringify({
type: 'chat',
from: userId,
content: message.content,
timestamp: Date.now()
}));
}
break;
case 'typing':
// Notify typing indicator
broadcastToRoom(message.roomId, {
type: 'typing',
userId,
isTyping: message.isTyping
});
break;
case 'cursor-move':
// Real-time cursor position (collaboration)
broadcastToRoom(message.roomId, {
type: 'cursor-move',
userId,
x: message.x,
y: message.y
}, [userId]); // Exclude sender
break;
}
});
ws.on('close', () => clients.delete(userId));
// Heartbeat / ping-pong
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// Detect dead connections
setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
Client with Reconnection Logic
// WebSocket client with auto-reconnect
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.handlers = new Map();
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectDelay = 1000; // Reset backoff
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const handler = this.handlers.get(data.type);
if (handler) handler(data);
};
this.ws.onclose = () => {
console.log(`WebSocket closed, reconnecting in ${this.reconnectDelay}ms`);
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
};
this.ws.onerror = (err) => console.error('WebSocket error:', err);
}
on(type, handler) { this.handlers.set(type, handler); }
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');
ws.on('chat', (msg) => appendMessage(msg));
ws.on('typing', (data) => showTypingIndicator(data));
HTTP/2 Server Push: The Misunderstood Technology
HTTP/2 Server Push allows the server to proactively send resources to the client before the client requests them. It's designed for asset preloading (CSS, JS, images), not for real-time data streaming.
Important clarification: HTTP/2 Server Push is not a replacement for SSE or WebSockets. It serves a completely different purpose. The name is misleading — it "pushes" static resources, not dynamic events.
// HTTP/2 Server Push (Node.js)
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
});
server.on('stream', (stream, headers) => {
if (headers[':path'] === '/index.html') {
// Push CSS before the browser even asks for it
stream.pushStream({ ':path': '/styles.css' }, (err, pushStream) => {
pushStream.respond({ ':status': 200, 'content-type': 'text/css' });
pushStream.end(fs.readFileSync('styles.css'));
});
// Push JS
stream.pushStream({ ':path': '/app.js' }, (err, pushStream) => {
pushStream.respond({ ':status': 200, 'content-type': 'application/javascript' });
pushStream.end(fs.readFileSync('app.js'));
});
// Respond with HTML
stream.respond({ ':status': 200, 'content-type': 'text/html' });
stream.end(fs.readFileSync('index.html'));
}
});
Status in 2026: HTTP/2 Server Push has been largely abandoned by the industry. Chrome removed support for HTTP/2 Server Push in Chrome 106 (2022), citing that it was rarely used correctly and often hurt performance. The recommended replacement is 103 Early Hints with Link: rel=preload headers.
The Complete Comparison Table
| Feature | SSE | WebSockets | HTTP/2 Push |
|---|---|---|---|
| Direction | Server → Client | Bidirectional | Server → Client (assets) |
| Protocol | HTTP | WebSocket (ws://) | HTTP/2 |
| Data format | Text (UTF-8) | Text + Binary | Any (resources) |
| Auto-reconnect | Built-in (browser) | Manual | N/A |
| Event replay | Last-Event-ID | Manual | N/A |
| Proxy-friendly | Yes (HTTP) | Sometimes problematic | Yes |
| Max connections | 6 per domain (HTTP/1.1) | Unlimited (separate protocol) | Multiplexed |
| Use case | Notifications, feeds, updates | Chat, gaming, collaboration | Asset preloading (deprecated) |
| Complexity | Low | Medium-High | Low (server config) |
Decision Framework: When to Use What
Use SSE When...
- Data flows in one direction only (server to client)
- You're building notifications, live feeds, dashboards, progress bars
- You want automatic reconnection and event replay
- You need to work through corporate proxies and CDNs
- You want minimal implementation complexity
Use WebSockets When...
- You need bidirectional communication (client sends data too)
- You're building chat, multiplayer games, collaborative editing
- You need to send binary data (audio, video, files)
- Latency requirements are sub-10ms
- You need more than 6 concurrent connections per domain
Use Neither (Use Polling) When...
- Updates are infrequent (< 1 per minute)
- Exact real-time delivery is not required
- You need to support very old browsers
- Your infrastructure doesn't support persistent connections (some serverless platforms)
My Opinionated Take
SSE should be your default for real-time features. Most real-time use cases are unidirectional — the server pushes updates, the client displays them. Notifications, live feeds, dashboards, stock tickers, sports scores, order status updates — all SSE. WebSockets add bidirectional complexity that 80% of "real-time" features don't need.
WebSockets are only justified for true bidirectional workloads. Chat is the canonical example. Gaming is another. Collaborative document editing (Figma, Google Docs) is another. If your client is only sending "I'm still here" pings, you don't need WebSockets.
HTTP/2 Server Push is dead. Stop considering it. Chrome dropped support. The ecosystem moved to 103 Early Hints. If you're reading an article from 2020 that recommends HTTP/2 Push for real-time data, that information is outdated.
Action Plan
Week 1: Evaluate Your Use Cases
- List all real-time features in your application (or planned features)
- For each, determine: is the client sending data back? (If no → SSE. If yes → WebSockets)
- Prototype both SSE and WebSocket implementations for your primary use case
- Measure: latency, connection stability, code complexity
Week 2: Implement
- Choose your technology and implement the server endpoint
- Build the client with proper reconnection and error handling
- Add authentication (cookies for SSE, token in URL/header for WS)
- Set up monitoring: connection count, message delivery rate, error rate
Week 3: Scale
- Add Redis pub/sub (or similar) for multi-instance message distribution
- Configure load balancer for long-lived connections (sticky sessions or connection draining)
- Implement graceful connection cleanup on server shutdown
- Load test with realistic connection counts and message rates
Sources and Further Reading
- MDN — Server-Sent Events API
- MDN — WebSocket API
- Chrome — Removing HTTP/2 Server Push
- WHATWG — Server-Sent Events Specification
- RFC 6455 — The WebSocket Protocol
- Smashing Magazine — SSE vs WebSockets Comparison
I'm Ismat, and I build BirJob — Azerbaijan's job aggregator scraping 80+ sources daily.
