Building Real-Time Features with WebSockets and SSE
A practical comparison of WebSockets, Server-Sent Events, and long polling with implementation guides for each.
Real-time features — live notifications, collaborative editing, chat, live dashboards, multiplayer games — are no longer a nice-to-have. Users expect instant updates. But choosing the right real-time technology is surprisingly nuanced. WebSockets, Server-Sent Events (SSE), and long polling each have distinct strengths, and picking the wrong one can lead to scaling nightmares, unnecessary complexity, or poor user experience.
We've built real-time systems for trading platforms, collaborative document editors, IoT dashboards, and live auction sites. This guide shares the decision framework we use and practical implementation patterns for each approach.
Decision Framework: Which Technology to Use
- Server-Sent Events (SSE): Best for one-way server-to-client streaming. Live feeds, notifications, stock tickers, log tailing. Simple, uses HTTP, auto-reconnects, works through most proxies and firewalls.
- WebSockets: Best for bidirectional communication. Chat, collaborative editing, multiplayer games. Full-duplex, low latency, but more complex infrastructure (sticky sessions, separate scaling).
- Long Polling: Fallback when SSE/WebSockets aren't available. Works everywhere, but high server overhead and higher latency. Use only as a last resort.
Server-Sent Events: The Underrated Choice
SSE is the most underused real-time technology. For 80% of real-time features (notifications, live updates, dashboards), you only need server-to-client communication — and SSE does this with zero dependencies, native browser support, automatic reconnection, and standard HTTP infrastructure.
// Next.js Route Handler with SSE
export async function GET(request: Request) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
// Send initial connection event
controller.enqueue(encoder.encode("event: connected\ndata: {}\n\n"));
// Subscribe to your event source (Redis pub/sub, DB changes, etc.)
const unsubscribe = eventBus.subscribe((event) => {
const data = JSON.stringify(event);
controller.enqueue(encoder.encode(`event: ${event.type}\ndata: ${data}\n\n`));
});
// Cleanup on client disconnect
request.signal.addEventListener("abort", () => {
unsubscribe();
controller.close();
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}WebSockets: When You Need Full-Duplex
WebSockets shine when clients need to send data frequently — chat messages, cursor positions, game inputs. The persistent connection eliminates the overhead of HTTP request/response cycles. However, WebSockets require careful infrastructure planning: sticky sessions for load balancing, separate health check mechanisms, and explicit reconnection logic.
class ReliableWebSocket {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectDelay = 30000;
connect(url: string) {
this.ws = new WebSocket(url);
this.ws.onopen = () => {
this.reconnectAttempts = 0; // Reset on successful connection
};
this.ws.onclose = (event) => {
if (!event.wasClean) {
this.scheduleReconnect(url);
}
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
}
private scheduleReconnect(url: string) {
// Exponential backoff with jitter
const delay = Math.min(
1000 * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
this.maxReconnectDelay
);
this.reconnectAttempts++;
setTimeout(() => this.connect(url), delay);
}
}Always implement exponential backoff with jitter for reconnection. Without jitter, all clients reconnect simultaneously after a server restart, causing a thundering herd that can crash your server again.
Scaling Real-Time Systems
The biggest challenge with real-time features is scaling beyond a single server. When you have multiple server instances, a message published on server A needs to reach clients connected to server B. The standard solution is a pub/sub backbone — Redis Pub/Sub for moderate scale, Apache Kafka or NATS for high throughput.
For SSE, this is straightforward: each server subscribes to the pub/sub channel and forwards events to its connected clients. For WebSockets, you additionally need sticky sessions (so clients reconnect to the same server) or a session state store that any server can read.
“Start with SSE. It's simpler, scales with standard HTTP infrastructure, and handles 80% of real-time use cases. Only reach for WebSockets when you genuinely need bidirectional communication — and when you do, invest in the infrastructure upfront.”
— Thomas Weber, Vaarak Architecture
Thomas Weber
Principal Software Architect