The Request-Response Constraint
HTTP's fundamental design is simple: the client sends a request, the server sends a response, and that's it. The server has no channel back to the client. It cannot say "hey, something changed" — it must wait until the client asks.
This was perfectly fine for HTTP's original purpose: fetching documents. But modern web applications aren't document viewers. They're chat apps, dashboards, collaborative editors, and live feeds. In these applications, the server frequently has new data that the client doesn't know about.
Client Server │ │ │──── GET /messages ───────────────────────>│ │<──── 200 OK (3 messages) ────────────────│ │ │ │ ... 10 seconds pass ... │ │ │ ← New message arrives │ │ from another user! │ │ │ Client has no idea. │ ← Server has no way │ Still showing 3 messages. │ to deliver it. │ │ │ Only when client asks again: │ │──── GET /messages ───────────────────────>│ │<──── 200 OK (4 messages) ────────────────│ │ │
When You Need More Than Request-Response
| Scenario | Why Request-Response Fails | What's Needed |
|---|---|---|
| Chat / messaging | Messages arrive at server from other users; your client doesn't know | Instant delivery of incoming messages |
| Live scores / tickers | Scores change continuously; client shows stale data | Continuous stream of updates |
| Collaborative editing | Multiple users change the same document simultaneously | Bidirectional, real-time sync |
| Notifications | Server-side events happen unpredictably | Push when event occurs |
| Multiplayer games | Other players' actions must appear immediately | Low-latency bidirectional communication |
The common thread: the server knows something the client doesn't, and waiting for the client to ask introduces unacceptable delay.
Polling
The simplest workaround: have the client ask repeatedly on a timer. If the server has new data, great. If not, it responds with "no change" and the client asks again later.
Client Server │ │ │──── GET /updates ────────────────────────>│ │<──── 200 OK (no change) ─────────────────│ │ │ │ ... wait 5 seconds ... │ │ │ │──── GET /updates ────────────────────────>│ │<──── 200 OK (no change) ─────────────────│ │ │ │ ... wait 5 seconds ... │ │ │ ← New data arrives! │──── GET /updates ────────────────────────>│ │<──── 200 OK (new data!) ─────────────────│ │ │ │ Latency: 0 to 5 seconds │
- Latency: 0 to N seconds (average N/2). If you poll every 5 seconds, updates appear 2.5 seconds late on average.
- Wasted requests: Most responses return "no change." The server does work for nothing.
- Server load scales linearly: 1,000 users polling every 2 seconds = 500 requests/second, regardless of whether anything changed.
- Simple to implement: Works everywhere. No special server support.
Long Polling
Long polling inverts the approach: instead of the server responding immediately with "no change," it holds the connection open until it has something to say (or a timeout expires). The client gets near-instant notification when events occur.
Client Server │ │ │──── GET /updates (long poll) ────────────>│ │ │ Server holds connection │ ... waiting ... │ open. No response yet. │ │ │ │ ← Event occurs! │<──── 200 OK (new data!) ─────────────────│ Server responds immediately │ │ │──── GET /updates (re-poll) ──────────────>│ Client re-requests instantly │ │ Server holds again... │ ... waiting ... │ │ │ │ timeout! ─│ │<──── 200 OK (no change, timeout) ────────│ After 30-60s, respond anyway │ │ │──── GET /updates (re-poll) ──────────────>│ Client re-requests │ │
Long polling is still just HTTP — it works through firewalls, proxies, and load balancers. Each request is a normal HTTP request; the only difference is the server takes longer to respond.
Historical note: Long polling powered early AJAX chat applications, the "Comet" pattern, and Facebook's original chat system (circa 2008). It was the dominant real-time technique before SSE and WebSockets gained browser support.
Server-Sent Events (SSE)
Server-Sent Events is a standardized HTTP streaming protocol: the server pushes events over a single long-lived HTTP connection. The key insight: SSE is just HTTP with Content-Type: text/event-stream and the connection held open. It is unidirectional: server → client only.
Client Server
│ │
│──── GET /events ─────────────────────────>│
│ Accept: text/event-stream │
│ │
│<──── 200 OK ─────────────────────────────│
│ Content-Type: text/event-stream │
│ Connection kept open │
│ │
│<──── data: {"user":"alice","msg":"hi"} │ Event 1
│ │
│<──── data: {"user":"bob","msg":"hello"} │ Event 2
│ │
│ ... minutes pass ... │
│ │
│<──── data: {"user":"alice","msg":"bye"} │ Event 3
│ │
│ Connection stays open indefinitely │
SSE Message Format
| Field | Purpose | Example |
|---|---|---|
data: |
The event payload (can span multiple lines) | data: {"score": 42} |
event: |
Named event type (triggers specific listeners) | event: notification |
id: |
Event ID for reconnection (browser sends Last-Event-ID) |
id: 1001 |
retry: |
Reconnection interval in milliseconds | retry: 5000 |
Built-in reconnection: If the connection drops, the browser automatically reconnects and sends the Last-Event-ID header. No reconnection logic needed on the client.
WebSockets
WebSockets are a separate protocol that starts as an HTTP request and then upgrades to a full-duplex, bidirectional communication channel. After the handshake, it is no longer HTTP — it's a different protocol with different framing.
Client Server │ │ │──── HTTP Request ────────────────────────>│ │ GET /chat HTTP/1.1 │ │ Upgrade: websocket │ │ Connection: Upgrade │ │ Sec-WebSocket-Key: dGhlIHNh... │ │ │ │<──── HTTP Response ──────────────────────│ │ HTTP/1.1 101 Switching Protocols │ │ Upgrade: websocket │ │ Sec-WebSocket-Accept: s3pPL... │ │ │ │ ══════════════════════════════════════ │ │ WebSocket connection established │ │ Full-duplex, bidirectional │ │ ══════════════════════════════════════ │ │ │ │──── "Hello from client" ─────────────────>│ │<──── "Hello from server" ────────────────│ │<──── "New message from Bob" ─────────────│ │──── "Thanks, got it" ───────────────────>│ │ │ │ Either side can send at any time │
When WebSockets shine: Chat applications, multiplayer games, collaborative editing, live trading platforms — anywhere both sides send data frequently and unpredictably.
Choosing the Right Approach
| Aspect | Polling | Long Polling | SSE | WebSockets |
|---|---|---|---|---|
| Direction | Client → Server | Client → Server | Server → Client | Bidirectional |
| Protocol | HTTP | HTTP | HTTP | WebSocket (after upgrade) |
| Latency | 0 to N seconds | Near-instant | Near-instant | Near-instant |
| Server load | High (constant requests) | Moderate | Low | Low |
| Auto-reconnect | N/A | Manual | Built-in | Manual |
| Proxy/firewall | Works everywhere | Works everywhere | Usually works | May be blocked |
| Best for | Low-frequency checks | Moderate real-time | Server push: feeds, progress | Chat, games, collaboration |
Decision Tree
Need bidirectional communication?
(Both client AND server send frequently)
│
├── YES ──→ WebSockets
│ (chat, games, collaboration)
│
NO
│
Server needs to push data to client?
│
├── YES ──→ Server-Sent Events (SSE)
│ (notifications, live feeds, progress)
│
NO
│
Data changes frequently (every <30 seconds)?
│
├── YES ──→ Long Polling
│ (near-real-time without special protocol)
│
NO ──→ Regular Polling
(dashboard refresh, status checks)
Rule of thumb: Start with the simplest approach that meets your requirements. Upgrade when you hit its limits, not before.
Implementation Realities
Tutorials make every technique look clean. Production is messier. Between your client and server sit proxies, CDNs, load balancers, and firewalls — each one can interfere with persistent connections.
| Technique | Corporate Proxy | CDN | Mobile Carrier | Load Balancer |
|---|---|---|---|---|
| Polling | Works | Works | Works | Works |
| Long Polling | Works (may timeout) | Works | Works (may timeout) | Works |
| SSE | May buffer/break | May buffer | May buffer | Needs config |
| WebSockets | May block upgrade | Requires WS support | Usually works | Needs sticky sessions |
Mobile Networks
- NAT timeout: Mobile carriers kill idle TCP connections after 30–60 seconds.
- Network switching: Moving from WiFi to cellular drops all TCP connections.
- Battery: Persistent connections prevent the cellular radio from sleeping.
Solutions: Send heartbeat/ping frames every 15–30 seconds. Implement auto-reconnect with exponential backoff (wait 1s, then 2s, then 4s, then 8s).
Beyond WebSockets
WebTransport (HTTP/3 + QUIC) provides bidirectional communication with multiple independent streams. GraphQL Subscriptions use WebSockets underneath with a query language. gRPC Streaming runs over HTTP/2 for service-to-service communication. All are variations on the same theme: letting the server send data without being asked.