Connections & Real-Time Communication

Polling, Long Polling, SSE, WebSockets — working around HTTP's request-response constraint

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               │
// Client-side polling with setInterval + fetch async function pollForUpdates() { try { const response = await fetch('/api/updates?since=' + lastTimestamp); const data = await response.json(); if (data.updates.length > 0) { renderUpdates(data.updates); lastTimestamp = data.timestamp; } } catch (err) { console.error('Poll failed:', err); } } // Poll every 5 seconds let lastTimestamp = Date.now(); setInterval(pollForUpdates, 5000);
  • 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   │
// Client: EventSource — built-in, no library needed const source = new EventSource('/api/events'); source.onmessage = (event) => { const data = JSON.parse(event.data); renderMessage(data); }; // Listen for named event types source.addEventListener('notification', (event) => { showNotification(JSON.parse(event.data)); }); source.onerror = (err) => { console.error('SSE connection error:', err); // Browser automatically reconnects! };

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      │
// Client: WebSocket API — built-in, no library needed const ws = new WebSocket('wss://example.com/chat'); ws.onopen = () => { console.log('Connected'); ws.send(JSON.stringify({ type: 'join', room: 'general' })); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); renderMessage(msg); }; ws.onclose = (event) => { console.log('Disconnected:', event.code, event.reason); // Reconnect logic goes here };

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.