sehaj/learn
System design/03 Networking and APIs

03.05 · concept · 11 min

Realtime: polling, SSE and WebSockets

Getting updates to a browser as they happen: polling, long polling, server-sent events and WebSockets, and what it takes to hold millions of open connections.

The problem: HTTP only answers questions

Snip adds a live counter to the dashboard: during a product launch, an owner wants to watch clicks tick up as they happen. But plain HTTP is request and response. The server can only speak when the browser asks. How does the server tell the browser "another click just happened"?

There are four standard answers, from simplest to most capable.

1. Polling

The browser asks every few seconds: "any new clicks?"

text
GET /links/x7Kp/stats   → { clicks: 18420 }
… 5 seconds later …
GET /links/x7Kp/stats   → { clicks: 18420 }      nothing new
… 5 seconds later …
GET /links/x7Kp/stats   → { clicks: 18431 }

Dead simple, works everywhere, and uses ordinary stateless servers and caches. But updates are up to one interval late, and most requests come back with nothing new. A thousand dashboards polling every 2 seconds is 500 requests a second, mostly wasted.

2. Long polling

The browser asks, and the server holds the request open until there's something to say (or a timeout of, say, 30 seconds passes), then answers. The browser immediately asks again. Updates arrive almost instantly with far fewer empty answers. The cost: every waiting browser holds an open request on a server.

3. Server-sent events (SSE)

The browser opens one ordinary HTTP request and the server keeps the response open, writing events into it whenever it likes:

text
GET /links/x7Kp/live
Accept: text/event-stream

← id: 1042
← data: {"clicks": 18431}

← id: 1043
← data: {"clicks": 18440}

It's one-way, server to browser, and plain HTTP, so it passes through proxies and load balancers easily. Browsers reconnect automatically and send the last event ID they saw, so the server can fill in anything missed.

4. WebSockets

A WebSocket starts as an HTTP request that asks to upgrade the connection. After that, it's a long-lived, two-way channel: either side can send messages at any time, with very little overhead per message. It's the right tool when the browser also sends a stream of messages: chat, multiplayer editing, games.

PollingLong pollingSSEWebSocket
DirectionClient asksClient asks, server waitsServer → clientBoth ways
DelayUp to the intervalNear instantNear instantNear instant
Wasted requestsManyFewNoneNone
Open connectionsNone between pollsOne per waiting clientOne per clientOne per client
Works with plain HTTP toolingYesYesYesNeeds upgrade support
Good forSlow-changing dataFallback, older setupsLive feeds, notifications, countersChat, collaboration, games

The hard part: holding connections at scale

Every SSE or WebSocket client is a connection that stays open for minutes or hours. That changes the design:

  • Servers hold state again. Each connection lives on one particular server. When a click for x7Kp is counted, the system has to find the server(s) holding connections that are watching x7Kp.
  • So you add a fan-out layer. Connection servers subscribe to the topics their clients care about, usually through a pub/sub system, and the code that counts clicks publishes to the topic.
  • Memory per connection adds up. Each open connection costs some memory and a file descriptor; a server can hold many thousands, but you size for connections, not just requests per second.
  • Deploys and failures cause reconnect storms. Restart a server holding 50,000 connections and 50,000 clients reconnect at once. Clients should wait a random short delay before reconnecting (03.09 Timeouts, retries, backoff and jitter).
Snip's live counter
subscribepublishDashboardsSSELoad balancerLive servershold connectionsPub/subtopic per linkStats workers
The counting path from Module 02 doesn't change; a separate set of servers holds the open connections and fans updates out to them.

4 steps. Use Next and Back to follow the request.

Text version
  1. An owner opens the dashboard. The browser opens an SSE stream, and the load balancer pins it to one live server for as long as it stays open.
  2. That live server subscribes to the topic for x7Kp, if it isn't subscribed already.
  3. When the stats workers fold a batch of clicks into the count (02.06), they also publish the new count to the topic.
  4. Pub/sub delivers it to every live server subscribed to x7Kp, and each writes it into the streams of its watching dashboards.
  • Dashboards → Load balancer
  • Load balancer → Live servers
  • Live servers → Pub/sub: subscribe
  • Stats workers → Pub/sub: publish

Snip uses SSE: updates only flow one way, it's plain HTTP, and reconnection is built in. Polling every 10 seconds would also have been reasonable for a first version; the click counts already lag by a few seconds.

Seen in production

Slack

2023

Real-time messaging

Every Slack client keeps a persistent WebSocket to Slack's servers. Clients connect to gateway servers in the nearest region, which track each user's channel subscriptions. Channel servers are stateful and in memory; each owns a subset of channels, assigned by consistent hashing. When a message arrives, its channel server sends it to every gateway subscribed to that channel, and Slack says messages are delivered across the world in about 500 ms.

What we foundPersistent WebSockets terminate on gateway servers; stateful channel servers, found by consistent hashing, fan messages out to them.

Real-time Messagingslack.engineering ↗
Predict first

You're adding live notifications ('your export is ready') to a web app. Users never send anything back over the channel. Which is the simplest good fit?

Commit to an answer before you read on.

Recap

Interview questions

MidfreqMust-knowConcept

Compare polling, long polling, server-sent events and WebSockets. When would you use each?

Report a mistakeEdit this page