AlgoInsight

Worked system design solution

Design: Polling vs Long-Polling vs SSE vs WebSocket

A comparative design exercise — when do you choose short polling, long polling, Server-Sent Events, or WebSockets for client-server real-time communication? Each has a place; the wrong choice cripples scalability or wastes bandwidth.

HLDEasy15 min readreal-timepollingwebsocketssetrade-offs

Requirements

When clients need server-pushed updates

The problem

A client (browser, mobile, IoT) needs to receive updates from the server. The server, not the client, knows when there's new data. How does the client find out?

Concrete scenarios:

Requirements

For each scenario:

The four mechanisms we compare

  1. Short polling — client asks "anything new?" every N seconds. Server responds immediately.
  2. Long polling — client asks "anything new?"; server holds the request open until new data arrives.
  3. Server-Sent Events (SSE) — server keeps a one-way HTTP stream open and pushes events.
  4. WebSocket — full-duplex persistent connection over TCP.

Interview: The interviewer often asks "design a chat app" or "design a stock ticker." The first decision is which of these mechanisms you pick — and why. The four trade off latency vs server-cost vs implementation complexity.

Mechanism Comparison

Latency, cost, complexity, browser support

The matrix

Mechanism Latency Server cost Bandwidth Complexity Direction
Short polling up to N sec High (request/sec) High Low Both (each poll)
Long polling < 1 sec Medium (holds conn) Medium Medium Both (each response)
SSE < 100 ms Low (1 conn / client) Low Low Server → client only
WebSocket < 50 ms Low (1 conn / client) Lowest High Full-duplex

When to pick which

Scenario Best mechanism Reasoning
Chat / messaging WebSocket Bidirectional + low latency for messages both ways
Stock ticker (read-only) SSE One-way push, simple HTTP, auto-reconnect
Notifications (rare events) SSE or long-poll Low frequency; persistent connection is overkill
Collaborative editor WebSocket Full-duplex, many small ops per second
Sports score updates SSE One-way, low frequency, mobile-friendly
Background sync (every 5 min) Short polling Cheap and simple; latency tolerance is high
Multiplayer game WebSocket Server pushes state + client sends input

Underlying transport

Mechanism Protocol Reconnect strategy
Short poll Plain HTTP None — just poll again
Long poll Plain HTTP (hold) Issue another GET on close
SSE HTTP/1.1 chunked Native auto-reconnect with Last-Event-Id
WebSocket TCP upgrade from HTTP Client-side manual reconnect logic

Architecture: Short Polling

Diagram + when it actually wins

How it works

sequenceDiagram
  autonumber
  participant C as Client
  participant S as Server
  loop every N seconds
    C->>S: GET /updates?since=T
    S-->>C: 200 [list of events]
  end

Pros

Cons

When it actually wins

Example flow

GET /v1/notifications?since=2026-05-24T11:59:00Z
{
  "events": [
    { "id": "ev_a", "type": "comment", "createdAt": "..." }
  ],
  "watermark": "2026-05-24T12:00:00Z"
}

Client uses watermark as the next since value to avoid re-fetching seen events.

Architecture: Long Polling

The intermediate sweet spot

How it works

sequenceDiagram
  autonumber
  participant C as Client
  participant S as Server
  C->>S: GET /updates?since=T
  Note over S: hold connection open<br/>up to 30s
  alt new data arrives
    S-->>C: 200 [event]
  else timeout
    S-->>C: 200 [] (empty)
  end
  C->>S: GET /updates?since=T2 (reconnect)

Pros

Cons

When it wins

Server-side requirement

The server needs to:

  1. Accept the GET and not respond immediately.
  2. Subscribe to an event source (Redis pub-sub, Kafka, etc.) keyed by since or user-id.
  3. Respond when an event arrives (or 30s timeout, whichever first).
app.get('/updates', async (req, res) => {
  const subscriber = redis.duplicate();
  const timer = setTimeout(() => { subscriber.disconnect(); res.json({ events: [] }); }, 30_000);
  subscriber.subscribe(`events:${req.user.id}`, () => {});
  subscriber.on('message', (channel, msg) => {
    clearTimeout(timer);
    subscriber.disconnect();
    res.json({ events: [JSON.parse(msg)] });
  });
  req.on('close', () => { clearTimeout(timer); subscriber.disconnect(); });
});

Architecture: Server-Sent Events

One-way streaming — the underrated workhorse

How it works

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: GET /events Accept: text/event-stream
  S-->>C: 200 + persistent connection
  loop server pushes
    S-->>C: data: event 1
    S-->>C: data: event 2
    S-->>C: : keep-alive
    S-->>C: data: event 3
  end
  Note over C,S: client disconnect/reconnect handled by browser

Wire format

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: { "type": "comment", "id": "..." }

id: 42
event: chat
data: { "from": "alice", "text": "hello" }

: this is a heartbeat / comment

Each event is separated by a blank line. Lines starting with : are comments (keep-alives).

Pros

Cons

When SSE wins

Client code

const es = new EventSource('/v1/events?token=...');
es.addEventListener('chat', (e) => {
  const data = JSON.parse(e.data);
  console.log('New message:', data);
});
es.onerror = () => console.log('Reconnecting...'); // browser auto-reconnects

Architecture: WebSocket

Full-duplex — the most powerful, most complex

How it works

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: GET /ws Upgrade: websocket
  S-->>C: 101 Switching Protocols
  Note over C,S: TCP connection now WebSocket
  par
    C->>S: msg "hello"
    S->>C: msg "welcome"
    C->>S: msg "..."
    S->>C: msg "..."
  end
  Note over C,S: ping/pong frames every 30s
  C->>S: close
  S->>C: close ack

Pros

Cons

When WebSocket wins

Server-side architecture

flowchart TB
  LB["L4 LB<br/>sticky on connection-id"]
  C1[Client 1] --> LB
  C2[Client 2] --> LB
  LB --> WS1["WS server 1<br/>holds 10K conns"]
  LB --> WS2["WS server 2<br/>holds 10K conns"]
  WS1 & WS2 <--> Pubsub["(Redis Pub/Sub<br/>fan-out across instances)"]

Heartbeats

WebSockets don't tell you the connection died until you try to send. Solution: send a ping every 30s; if no pong in 30s, close the socket.

setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) ws.ping();
}, 30_000);

Scaling Real-Time at Fleet Scale

Pub/Sub fan-out, connection sharding, partition strategies

The fan-out problem

A message addressed to user U must reach U's connection — but U might be connected to any of N WebSocket servers.

Solution: Pub/Sub layer

flowchart LR
  Source["Event source<br/>backend API"] --> Pub["Publish on Redis<br/>channel = user:U"]
  Pub --> WS1["WS-server-1<br/>SUBSCRIBE user:U"]
  Pub --> WS2["WS-server-2<br/>SUBSCRIBE user:U"]
  WS1 -->|"if U connected here"| U1[User U's socket]
  WS2 -.->|"U not connected here"| Skip[ignore]

Each WS server subscribes to channels for the users it's hosting (efficient — wildcard subscriptions don't scale). When a new connection arrives, the server SUBSCRIBE-s to that user's channel.

Connection sharding

For very large fleets:

Backpressure

Slow clients accumulate undelivered messages in the server's send buffer.

Strategy Behavior
Drop oldest Discard old messages when buffer is full
Drop newest Reject new sends when buffer is full
Disconnect Close the slow connection (force reconnect)

For chat: drop oldest is reasonable. For trades: disconnect (the client must catch up explicitly).

At scale numbers

Fleet size Recommendation
1K clients Single Node/Go process
100K clients 10-20 WS servers + Redis Pub/Sub
1M clients Sharded WS servers + Kafka for durable fan-out + sticky LB
10M+ clients Multi-region; per-region fan-out; geo-routing

Decision Framework

How to pick — and when to combine

The decision tree

flowchart TD
  Q1{Need bidirectional<br/>real-time?}
  Q1 -->|Yes| WS[WebSocket]
  Q1 -->|No| Q2{Need < 1s latency?}
  Q2 -->|No| Poll[Short polling]
  Q2 -->|Yes| Q3{Many clients,<br/>frequent updates?}
  Q3 -->|Yes| SSE[Server-Sent Events]
  Q3 -->|No| LP[Long polling]

Hybrid patterns

In production, you often combine:

Pattern 1: SSE + plain HTTP POST

Pattern 2: WS for the active tab + polling when backgrounded

Pattern 3: WS with HTTP fallback

The trade-off summary

Interview: If you can only remember one rule: pick the simplest mechanism that meets your latency requirement. Don't reach for WebSocket because it's powerful — reach for it because the application genuinely needs full-duplex. Many "real-time" features ship perfectly well on SSE or even long-polling, with less operational complexity.

Real-world examples

Product Mechanism Why
Slack messages WebSocket Chat, full-duplex, low latency
Twitter notifications SSE Server-push, no client-send needed
GitHub Actions logs SSE Log streaming, one-way
Trello board updates WebSocket Multi-user collaboration
Build status badge Short poll Status check; latency tolerance high
Stripe webhooks (server→server) HTTP POST Different problem entirely (push from S1 to S2)

Practice this question