Long Polling vs SSE vs WebSocket
Long polling fakes push with held requests, SSE streams one-way over HTTP natively, and WebSocket opens a full bidirectional channel — pick based on data flow direction and infrastructure constraints.
The Problem
Every real-time web application faces the same question: how does the server push data to the client? HTTP was designed for request/response, not push. Three approaches have emerged — long polling, SSE, and WebSocket — each with different trade-offs in complexity, compatibility, performance, and scalability. Choosing wrong means over-engineering or under-delivering.
Mental Model
Like different communication methods — polling is checking the mailbox every 5 minutes, long polling is sitting at the mailbox waiting until mail arrives, SSE is a one-way intercom where the office broadcasts announcements, and WebSocket is a phone call where both sides can talk at any time.
Architecture Diagram
How It Works
The web was built on a simple model: the client asks, the server answers. But modern applications need the server to push data to the client — chat messages, notifications, live scores, AI token streams, collaborative edits. Three techniques evolved to solve this, each from a different era of web development and with fundamentally different trade-offs.
Long Polling: The Original Hack
Long polling emerged in the mid-2000s when developers realized they could abuse HTTP's connection model. Instead of the server responding immediately, it holds the request open until it has something to send.
// Client-side long polling
async function longPoll() {
try {
const response = await fetch('/api/updates?since=' + lastEventId);
const data = await response.json();
lastEventId = data.lastId;
handleUpdates(data.events);
} catch (err) {
// Wait before retrying on error
await new Promise(resolve => setTimeout(resolve, 3000));
}
// Immediately reconnect for next batch
longPoll();
}
longPoll();
// Server-side (Express)
app.get('/api/updates', async (req, res) => {
const since = req.query.since || 0;
// Check if data is already available
let events = await getEventsSince(since);
if (events.length > 0) {
return res.json({ events, lastId: events.at(-1).id });
}
// Wait for new data (with timeout)
const timeout = setTimeout(() => {
res.json({ events: [], lastId: since });
}, 30000); // 30-second timeout
eventEmitter.once('newEvent', (event) => {
clearTimeout(timeout);
res.json({ events: [event], lastId: event.id });
});
});
The cycle is: request → server holds → response arrives → client immediately reconnects. This creates the illusion of server push, but every event requires a full HTTP request/response cycle with headers.
When long polling is actually the right choice:
- Firewalls block WebSocket and the SSE polyfill does not work
- Maximum compatibility is required (works in literally every HTTP client)
- Events are infrequent (once per minute) and the reconnection overhead is negligible
- The team is building a quick prototype and does not want to set up WebSocket infrastructure
Server-Sent Events: HTTP-Native Push
SSE is what long polling was trying to be. The client opens one HTTP connection, and the server streams events over it indefinitely.
// Client: trivially simple
const source = new EventSource('/api/stream');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};
// Auto-reconnects on disconnect
// Sends Last-Event-ID header on reconnect
// Handles event types, retry intervals — all built in
// Server
app.get('/api/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const send = (data, id) => {
res.write(`id: ${id}\ndata: ${JSON.stringify(data)}\n\n`);
};
const handler = (event) => send(event, event.id);
eventBus.on('update', handler);
req.on('close', () => eventBus.off('update', handler));
});
SSE delivers everything long polling tried to achieve but with zero reconnection overhead, built-in event IDs for resumption, and the server controls the timing completely.
WebSocket: The Full Duplex Channel
WebSocket starts as HTTP, then upgrades to a completely different protocol. After the handshake, HTTP is gone — what remains is a raw bidirectional byte pipe with minimal framing.
// Client
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', channel: 'trades' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onclose = (event) => {
// Application must handle reconnection
console.log(`Closed: ${event.code} ${event.reason}`);
setTimeout(reconnect, 1000 * Math.min(attempts++, 30));
};
The upgrade handshake:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After this handshake, both sides can send frames at any time. No request/response. No headers per message. Just 2-14 bytes of framing overhead per message.
The Decision Matrix
This is the comparison table that matters when making an architecture decision:
| Criterion | Long Polling | SSE | WebSocket |
|---|---|---|---|
| Direction | Server → Client (simulated) | Server → Client | Bidirectional |
| Latency | Medium (reconnect gap) | Low (persistent stream) | Lowest (always open) |
| Complexity | Low (just HTTP) | Low (EventSource API) | Medium (protocol upgrade, reconnect logic) |
| Scalability | Poor (constant reconnections) | Good (one connection per stream) | Good (one connection, but stateful) |
| Proxy compatibility | Excellent (plain HTTP) | Good (HTTP, but buffering proxies can interfere) | Poor (requires Upgrade header support) |
| Auto-reconnect | Manual (implement it) | Built-in (with Last-Event-ID) | Manual (implement it) |
| Binary support | Yes (as base64) | No (text only) | Yes (native binary frames) |
| HTTP/2 multiplexing | Yes | Yes (multiple streams, one TCP) | No (uses its own framing) |
| Browser support | Universal | All modern browsers | All modern browsers |
| Firewall friendly | Yes (plain HTTP) | Yes (plain HTTP) | Sometimes no (Upgrade blocked) |
| Per-message overhead | ~300 bytes (full HTTP headers) | ~5 bytes (data: + newlines) | 2-14 bytes (frame header) |
| Max connections per domain | 6 (HTTP/1.1) | 6 (HTTP/1.1), unlimited (HTTP/2) | Not limited by HTTP |
Real Architecture Decisions
Here is how this decision plays out for real products:
Notification System (Slack-like)
Choose: WebSocket. Users both send and receive messages. The client sends typing indicators, read receipts, and messages. The server pushes messages from other users. Bidirectional is essential.
Live Dashboard (Grafana-like)
Choose: SSE. Data flows one way — the server pushes metric updates. The client only consumes. SSE's auto-reconnect and Last-Event-ID ensure no data gaps. Multiple SSE streams multiplex over HTTP/2.
AI Chat Interface (ChatGPT-like)
Choose: SSE. The user sends a prompt via POST request. The server streams tokens back via SSE. During generation, communication is strictly one-way. The next prompt is a new POST request.
Collaborative Editor (Google Docs-like)
Choose: WebSocket. Both users send edits simultaneously. Operations must be applied in near-real-time in both directions. The operational transform or CRDT protocol requires bidirectional low-latency messaging.
Stock Ticker (Bloomberg-like)
Choose: WebSocket. Sub-millisecond latency matters. Binary data (packed price updates) is efficient. Traders also send orders through the same connection. The infrastructure team can configure proxies to support WebSocket.
Legacy Enterprise App
Choose: Long Polling. The corporate proxy blocks WebSocket upgrades. SSE works but IT has not approved the content type. Long polling is plain HTTP and works through every proxy, firewall, and security appliance.
Scaling Considerations
Each approach has different operational characteristics at scale:
Long Polling at Scale: Each "push" costs a full HTTP request/response. With 100,000 connected users and 1 event per second, that is 100,000 HTTP requests per second just for reconnection. The connection churn is expensive. Facebook made this work in the early days with their Erlang-based Comet server, but it was not pleasant.
SSE at Scale: 100,000 persistent connections, each consuming a file descriptor and a small amount of memory. Node.js handles this well with its event loop. The key challenge is that if a server process restarts, all clients reconnect simultaneously (thundering herd). Implement jittered reconnection using the retry field.
# Send a random retry interval to spread reconnections
retry: 3000 # Client A reconnects in 3s
retry: 5000 # Client B reconnects in 5s
retry: 4000 # Client C reconnects in 4s
WebSocket at Scale: Similar to SSE in terms of persistent connections, but WebSocket connections are stateful — they often carry session data, authentication state, and subscription information. This makes horizontal scaling harder because connection state must be considered when routing. Use a pub/sub backbone (Redis, NATS) to propagate events across server instances.
The HTTP/2 Factor
HTTP/2 significantly changes the SSE vs WebSocket calculus. Under HTTP/1.1, each SSE stream consumed one of the browser's 6-per-domain connections. Three SSE streams meant only 3 connections left for API calls.
HTTP/2 multiplexes all streams over a single TCP connection. A single browser tab can sustain 100 SSE streams without consuming additional connections. This eliminates SSE's biggest operational limitation and makes it viable for complex dashboards with many independent data streams.
WebSocket does not benefit from HTTP/2 multiplexing. After the upgrade handshake, it operates on its own framing protocol over a dedicated TCP connection. Each WebSocket connection is still a separate TCP socket.
This is why the industry trend is shifting back toward SSE for server-push use cases. It is simpler, more compatible, and with HTTP/2, more efficient. Reserve WebSocket for genuine bidirectional needs — fewer use cases require it than most teams expect.
Key Points
- •Long polling is the simplest to implement and works everywhere, but wastes resources on constant reconnection and cannot push data faster than the reconnect cycle.
- •SSE is HTTP-native, auto-reconnects with Last-Event-ID, and works through proxies and CDNs — but is strictly one-way (server to client).
- •WebSocket provides true bidirectional communication with minimal framing overhead, but requires special proxy configuration and has no built-in reconnection.
- •For 90% of real-time needs (notifications, feeds, dashboards, AI streaming), SSE is the right choice. WebSocket is only necessary when the client sends frequent data too.
- •HTTP/2 changes the equation significantly — SSE over HTTP/2 multiplexes perfectly, eliminating the connection-per-stream limitation that plagued SSE over HTTP/1.1.
Key Components
| Component | Role |
|---|---|
| Long Polling | Client sends HTTP request, server holds it open until data is available or timeout, client immediately reconnects after receiving response |
| Server-Sent Events (SSE) | Server pushes events over a persistent HTTP connection using text/event-stream format with auto-reconnect and event IDs |
| WebSocket | Full-duplex bidirectional connection over a single TCP socket, upgraded from HTTP via 101 Switching Protocols handshake |
| EventSource API | Browser-native API for SSE that handles connection lifecycle, auto-reconnection, and event parsing automatically |
| HTTP Upgrade Mechanism | The protocol switch from HTTP to WebSocket via Upgrade header — the only moment where HTTP is involved in a WebSocket connection |
When to Use
Use long polling only as a fallback when SSE and WebSocket are unavailable. Use SSE for server-to-client push (notifications, feeds, streaming responses, dashboards). Use WebSocket for true bidirectional communication (chat, collaborative editing, multiplayer games, trading platforms).
Tool Comparison
| Tool | Type | Best For | Scale |
|---|---|---|---|
| Socket.IO | Open Source | WebSocket with automatic fallback to long polling, rooms, namespaces, and reconnection built in | Small-Enterprise |
| Native EventSource API | Open Source | Zero-dependency SSE consumption in browsers with automatic reconnection | Small-Enterprise |
| SockJS | Open Source | WebSocket emulation with fallback transports for environments where WebSocket is blocked | Small-Medium |
| Centrifugo | Open Source | Language-agnostic real-time messaging server supporting WebSocket, SSE, and HTTP streaming with pub/sub | Medium-Enterprise |
Debug Checklist
- For long polling: check server-side timeout configuration — most proxies kill idle connections after 60 seconds, so the long-poll timeout must be shorter.
- For SSE: verify Content-Type is text/event-stream and no buffering proxy (nginx, CloudFront) is holding the response body.
- For WebSocket: confirm the HTTP Upgrade header passes through all proxies — use browser DevTools Network tab to verify the 101 response.
- For all three: test on corporate networks with restrictive firewalls. WebSocket often fails, SSE usually works, long polling always works.
- Check connection limits: HTTP/1.1 allows 6 connections per domain. Multiple SSE or long-poll connections consume these slots. HTTP/2 eliminates this problem.
Common Mistakes
- Defaulting to WebSocket for every real-time feature. Most use cases are server-push only, where SSE is simpler and more reliable.
- Implementing long polling without a timeout. The server must eventually respond (even with empty data) or proxies, load balancers, and browsers will kill the connection.
- Not handling WebSocket reconnection. Unlike SSE, WebSocket has no auto-reconnect. The application must implement retry logic, exponential backoff, and state recovery.
- Ignoring proxy and firewall compatibility. WebSocket requires proxy support for the Upgrade header. In corporate environments, this frequently fails silently.
- Using long polling when SSE is available. Long polling made sense in 2010 when IE did not support SSE. Today, EventSource is supported in all modern browsers.
Real World Usage
- •Slack uses WebSocket for real-time messaging because chat requires bidirectional communication — both sending and receiving messages with minimal latency.
- •ChatGPT uses SSE to stream AI responses because the data flow is strictly server-to-client during generation.
- •Trello originally used long polling for board updates before migrating to WebSocket as their real-time requirements grew.
- •Facebook's early chat system used long polling (Comet) before WebSocket existed, handling millions of concurrent held connections.
- •Stock trading platforms use WebSocket for bidirectional order placement and real-time price feeds with sub-millisecond requirements.