WebSockets vs HTTP Polling: Choosing the Right Real-Time Pattern
When WebSockets are the right tool, when long-polling is simpler, when Server-Sent Events are enough — and the performance tradeoffs that determine which pattern fits your use case.
The real-time problem: HTTP is request-response by design
HTTP was designed around a request-response cycle: the client asks, the server answers, the connection closes. This works well for most of the web, but fails for use cases where data changes on the server and clients need to know about it immediately — without having to ask first. A chat message, a stock price update, a live scoreboard, a collaborative document edit — all of these involve the server pushing data to clients unprompted.
The three main patterns for real-time data delivery — short polling, long polling, and WebSockets (plus the closely related Server-Sent Events) — are different strategies for working around or extending HTTP's request-response model. Each has a sweet spot, and choosing the wrong one leads to either unnecessary complexity or unacceptable performance.
Short polling: simple, wasteful, sometimes right
Short polling is the simplest approach: the client makes a regular HTTP request every N seconds to check for new data. If there is new data, the server returns it. If there is not, the server returns an empty or unchanged response.
// Short polling — check every 5 seconds
function startPolling() {
return setInterval(async () => {
const res = await fetch('/api/notifications');
const data = await res.json();
if (data.items.length > 0) {
displayNotifications(data.items);
}
}, 5000);
}
const poller = startPolling();
// Stop when no longer needed
clearInterval(poller);The cost is obvious: most requests return no new data, consuming server resources, network bandwidth, and battery on mobile devices for no benefit. With 1,000 clients polling every 5 seconds, the server handles 200 requests per second regardless of whether anything changed. Short polling is appropriate when update latency of several seconds is acceptable, the polling interval can be long, or the implementation simplicity outweighs the efficiency cost.
Long polling: efficient short polling
Long polling improves on short polling by having the server hold the connection open until new data is available or a timeout expires. The client sends a request; the server does not respond until it has something to say. When data arrives (or a timeout fires), the server responds and the client immediately sends another request to wait for the next update.
// Long polling — client side
async function longPoll(lastEventId) {
try {
const res = await fetch(`/api/events?after=${lastEventId}`, {
signal: AbortSignal.timeout(30000) // 30s timeout
});
const data = await res.json();
if (data.events.length > 0) {
processEvents(data.events);
lastEventId = data.events.at(-1).id;
}
} catch (err) {
if (err.name !== 'TimeoutError') {
await sleep(1000); // back off on real errors
}
}
// Immediately reconnect
longPoll(lastEventId);
}
// Server side (Node.js / Express)
app.get('/api/events', async (req, res) => {
const after = req.query.after;
// Wait up to 29s for new events
const events = await waitForEvents(after, { timeout: 29000 });
res.json({ events });
});Long polling is efficient when updates are infrequent — the server connection only closes (and a new request is made) when data actually arrives. The downside is that each client holds an open HTTP connection, which consumes server memory and file descriptors. At very high connection counts, this becomes significant. Long polling also has higher latency than WebSockets because each exchange involves HTTP overhead.
Server-Sent Events: one-way push over HTTP
Server-Sent Events (SSE) is an HTTP standard where the server sends a stream of text events to the client over a single long-lived HTTP connection. The client opens one connection, and the server pushes events as they happen. The browser handles reconnection automatically if the connection drops.
// Client-side SSE (built into browser)
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};
eventSource.addEventListener('price-update', (event) => {
updatePrice(JSON.parse(event.data));
});
eventSource.onerror = () => {
// Browser reconnects automatically
};
// Server-side SSE (Node.js)
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const send = (eventType, data) => {
res.write(`event: ${eventType}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
const interval = setInterval(() => {
send('price-update', { symbol: 'BTC', price: getPriceNow() });
}, 1000);
req.on('close', () => clearInterval(interval));
});SSE is simpler than WebSockets for use cases where the client only needs to receive data, not send it. It works over plain HTTP/2 (multiplexed over a single TCP connection), respects HTTP semantics (caching, authentication headers), and automatic reconnection with last-event-id tracking is built into the browser spec. Use SSE for dashboards, live feeds, notifications, and streaming AI responses — anything where data flows one direction from server to client.
WebSockets: full-duplex bidirectional communication
A WebSocket is a persistent connection that starts as an HTTP upgrade handshake and then operates as a bidirectional TCP stream. Both client and server can send messages at any time without the overhead of HTTP headers on each message.
// Client-side WebSocket
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleMessage(msg);
};
ws.onclose = (event) => {
// Implement exponential backoff reconnection
setTimeout(() => reconnect(), Math.min(30000, delay * 2));
};
// Server-side (Node.js with 'ws' library)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
ws.on('message', (rawData) => {
const msg = JSON.parse(rawData.toString());
if (msg.type === 'subscribe') {
addToChannel(ws, msg.channel);
}
});
ws.on('close', () => removeFromAllChannels(ws));
});
// Broadcast to all connected clients in a channel
function broadcast(channel, data) {
for (const client of channels.get(channel) ?? []) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
}
}WebSockets are the correct choice for collaborative editing (multiple users editing the same document), multiplayer games, trading platforms where the client sends orders and receives real-time market data, and any application where both directions need to carry frequent, low-latency messages.
The complexity cost is real: WebSockets do not automatically reconnect, do not work transparently through all proxies and load balancers without configuration (sticky sessions or a message broker like Redis Pub/Sub is usually needed for horizontal scaling), and authentication via headers is not straightforward since the WebSocket upgrade request follows different rules than regular HTTP requests.
Decision guide
Short polling: update frequency of 30+ seconds is acceptable, implementation must be trivially simple, or the data source is a standard REST API you do not control.
Long polling: updates are infrequent but must be near-instant when they occur, you need to support environments where WebSockets may be blocked, or you want real-time behavior with only HTTP infrastructure.
Server-Sent Events: data flows from server to client only (dashboards, feeds, notifications, AI token streaming), and you want HTTP's built-in authentication and caching without the complexity of WebSockets.
WebSockets: both client and server send messages frequently, latency must be minimal (under 100ms), or you are building a collaborative or interactive real-time feature that cannot tolerate polling overhead.