The Real-Time Communication Problem
When you need a web app to receive updates from the server — chat messages, live scores, stock prices, notifications — you have three main options: WebSocket, Server-Sent Events (SSE), and long polling. They differ fundamentally in how the connection works, what they cost, and when they fail.
Choosing the wrong one leads to wasted resources, scaling headaches, or poor user experience. This guide explains exactly how each works and gives clear criteria for choosing.
Long Polling
How It Works
Long polling is the oldest technique and requires no special browser APIs.
- Client sends a regular HTTP request
- Server holds the connection open (doesn't respond immediately)
- When new data is available, the server responds
- Client immediately sends another request
- Repeat
// Client
async function longPoll() {
while (true) {
try {
const res = await fetch('/api/events?lastId=' + lastEventId);
const data = await res.json();
processEvent(data);
lastEventId = data.id;
} catch (err) {
// Connection dropped, wait before retry
await new Promise(r => setTimeout(r, 2000));
}
}
}
longPoll();
// Server (Node.js / Express)
app.get('/api/events', async (req, res) => {
const lastId = req.query.lastId;
// Wait for up to 30 seconds for new data
const data = await waitForNewData(lastId, { timeout: 30000 });
if (data) {
res.json(data);
} else {
res.status(204).end(); // timeout, no new data
}
});
Pros and Cons
Pros:
- Works everywhere — any HTTP server, any client
- Passes through firewalls and proxies transparently
- Easy to implement with existing infrastructure
- Works with standard load balancers without sticky sessions
Cons:
- HTTP overhead on every request (headers, TCP handshake overhead in HTTP/1.1)
- Higher latency — minimum of one round-trip per event
- Server holds connections open, consuming threads/memory
- Not truly bidirectional — the client must always initiate
Best for: Environments with strict infrastructure constraints, simple notification systems where updates are infrequent, or legacy systems.
Server-Sent Events (SSE)
How It Works
SSE uses a single, persistent HTTP connection that the server can push data through at any time.
- Client opens one HTTP connection with
EventSource - Server streams
text/event-streamdata - Connection stays open; server sends events whenever they occur
- Browser automatically reconnects if the connection drops
// Client — just 2 lines
const es = new EventSource('/api/stream');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
// Named event types
es.addEventListener('user-joined', (event) => {
console.log('User joined:', event.data);
});
es.addEventListener('error', () => {
if (es.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
});
// Server (Node.js / Express)
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send a comment to keep the connection alive
const keepAlive = setInterval(() => {
res.write(': ping\n\n');
}, 20000);
// Send data
const sendEvent = (type, data) => {
res.write(`event: ${type}\n`);
res.write(`data: ${JSON.stringify(data)}\n`);
res.write(`id: ${Date.now()}\n\n`); // id for reconnect recovery
};
// Subscribe to your event bus
const unsubscribe = eventBus.on('update', (data) => sendEvent('update', data));
req.on('close', () => {
clearInterval(keepAlive);
unsubscribe();
});
});
Pros and Cons
Pros:
- Native browser reconnection with
Last-Event-IDheader - Simpler than WebSocket — plain HTTP, works with standard infrastructure
- Excellent browser support (all modern browsers)
- Events can have types, IDs, and retry intervals
- Works over HTTP/2 with multiplexing (many streams per connection)
Cons:
- Unidirectional — server to client only; client sends separate requests
- Limited to 6 concurrent connections per domain in HTTP/1.1 (HTTP/2 removes this limit)
- Text-only (UTF-8); can't stream binary data efficiently
- Some corporate proxies buffer responses and break streaming
Best for: Notifications, live feeds, dashboards, real-time logs — any scenario where data flows server → client and the client sends occasional updates via regular POST requests.
WebSocket
How It Works
WebSocket starts as an HTTP connection, then upgrades to a full-duplex TCP connection via the Upgrade handshake.
- Client sends HTTP Upgrade request
- Server responds with
101 Switching Protocols - Both ends can now send messages at any time, in either direction
- Messages can be text or binary (ArrayBuffer / Blob)
// Client
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleMessage(msg);
};
ws.onclose = (event) => {
console.log('Closed:', event.code, event.reason);
// Implement exponential backoff reconnect
setTimeout(reconnect, Math.min(30000, 1000 * 2 ** retryCount++));
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Send from client
function sendMessage(data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
// Server (Node.js / ws library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
console.log('Client connected from', req.socket.remoteAddress);
ws.on('message', (message) => {
const data = JSON.parse(message);
// handle data.type === 'subscribe', 'unsubscribe', 'message', etc.
handleClientMessage(ws, data);
});
ws.on('close', (code, reason) => {
console.log('Client disconnected:', code, reason.toString());
cleanupClient(ws);
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
// Ping to detect stale connections
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// Heartbeat — terminate broken connections
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
Pros and Cons
Pros:
- True bidirectional communication — client and server both push at any time
- Low overhead per message after the initial handshake (2–10 byte frame header vs hundreds of bytes for HTTP)
- Supports binary data (images, audio, file chunks)
- Very low latency for high-frequency messaging (gaming, trading)
Cons:
- Requires WebSocket support in infrastructure (load balancers, proxies must be configured)
- No automatic reconnection — you must implement it yourself
- Stateful connections complicate horizontal scaling (sticky sessions or pub/sub layer needed)
- Firewall/proxy issues in restrictive corporate networks
Best for: Chat, multiplayer games, live collaboration (Google Docs-style), financial trading dashboards, any app where the client also sends frequent messages.
Side-by-Side Comparison
| Feature | Long Polling | SSE | WebSocket |
|---|---|---|---|
| Direction | Client → Server → Client | Server → Client | Bidirectional |
| Protocol | HTTP | HTTP | WebSocket (TCP) |
| Binary data | Via encoding | No | Yes |
| Auto-reconnect | Manual | Built-in | Manual |
| Browser support | Universal | All modern | All modern |
| HTTP/2 support | Yes | Multiplexed | N/A |
| Proxy/firewall friendly | ✅ Best | ✅ Good | ⚠️ Configure needed |
| Message overhead | High (full HTTP) | Low | Minimal |
| Scaling complexity | Low | Low-Medium | High |
Decision Guide
Use Long Polling if:
- You need maximum infrastructure compatibility
- Updates are infrequent (< once per 30 seconds)
- You're behind strict proxies or older load balancers
- You need a quick implementation without new infrastructure
Use SSE if:
- Data flows primarily server → client
- You want simplicity with native browser reconnection
- You're building notifications, live feeds, progress updates
- You're using HTTP/2 (SSE becomes very efficient)
Use WebSocket if:
- You need true bidirectional messaging (chat, games, collaboration)
- You're sending binary data
- Message frequency is high (> a few per second)
- Latency is critical (trading, gaming)
Scaling Considerations
Long polling and SSE are easier to scale because they use standard HTTP — any stateless load balancer works. WebSocket connections are stateful, so a load balancer must route the same client to the same server (sticky sessions), or you need a pub/sub layer (Redis, NATS) so any server can publish to any connected client.
For high-scale WebSocket deployments, look at: Socket.IO (handles reconnection, rooms), Ably, Pusher, or managed WebSocket services.
→ Use the URL Parser to inspect WebSocket endpoint URLs and their query parameters.