System designCore10 min

WebSockets & long polling

HTTP lets a browser ask and a server answer, but never lets the server speak first. Live scores, chat and bidding all need exactly that. This page covers the four ways to fake it or fix it, from polling to WebSockets, and what long-lived connections do to the servers that hold them.

1 · The problem

Asking again and again trades load for delay

A charity runs a live online auction. Lot 42, a vintage guitar, has 20,000 people watching. A new bid arrives about every 30 seconds, and everyone watching should see it within about a second.

In plain HTTP the server can only answer requests, so the simplest plan is short polling: every watcher's page asks "anything new?" every few seconds. The load is the number of watchers divided by the interval, and the average wait to see a bid is half the interval:

05,00010,00015,00020,000246810Seconds between polls (s)Requests per second

Requests per second = 20,000 ÷ interval. Polling every 2 seconds gives 10,000 requests a second for an average delay of 1 second. Halving the delay doubles the load.

Worse, almost all of that work is wasted. With a bid every 30 seconds on average, only about 1 poll in 15 finds anything new; the other 14 carry full HTTP headers, cookies and authentication checks to say nothing. Short polling is still the right choice when updates are rare and a delay of minutes is fine, such as checking whether a report is ready.

2 · Long polling

Long polling holds the question open until there is an answer

Long polling keeps the same request-and-response shape but changes when the server answers. The browser asks; the server doesn't reply until it has something to say, or until a timeout passes; the browser immediately asks again:

Each request names the last bid it saw, so nothing is missed between requests. A timeout of about 30 seconds keeps proxies from cutting the waiting request off.

For the auction this is a large win. Each request now waits until the next bid or the 30-second timeout, about 19 seconds on average, so 20,000 watchers make about 20,000 ÷ 19 ≈ 1,050 requests a second instead of 10,000, and a bid reaches them almost at once.

The costs that remain:

  • A header per message. Every bid still costs a full HTTP request and response.
  • A gap after each answer. If two bids arrive a moment apart, the second waits while the first answer travels back and the next request travels out: three trips across the network.
  • Waiting requests hold resources. 20,000 open requests are 20,000 connections and request slots on the servers.
  • Several servers need care. The follow-up request may land on a different server, so the "after bid 18" position must be meaningful on any of them.

3 · Server-sent events

Server-sent events stream updates one way over plain HTTP

Most watchers never bid; they only listen. For one-way updates, browsers have server-sent events (SSE): the page opens one ordinary HTTP request, and the server never finishes the response. It keeps writing events into it, in a simple text format of type text/event-stream:

The response stream, as the browser receives it

HTTP/1.1 200 OK
Content-Type: text/event-stream

id: 18
event: bid
data: {"lot": 42, "amount": 2400}

id: 19
event: bid
data: {"lot": 42, "amount": 2500}

Each event ends with a blank line. The browser's EventSource API reconnects by itself if the connection drops, and sends back the last id it received in a Last-Event-ID header, so the server can replay what was missed. Because it is plain HTTP, SSE passes through most proxies and load balancers without special handling.

Its limits:

  • One direction only. A bid goes up as a normal HTTP request.
  • Text only, which is fine for JSON.
  • A connection cap on HTTP/1.1. Browsers allow about 6 connections per site across all tabs, so a seventh tab's stream waits. Over HTTP/2, each stream shares one connection, and the default limit is about 100 (HTTP).

4 · WebSockets

A WebSocket turns one HTTP request into a two-way pipe

Bidders need both directions, quickly: send a bid, see everyone else's. A WebSocket starts as an HTTP request that asks to switch protocols. If the server agrees, the same TCP connection stops speaking HTTP and becomes a channel where either side can send a message at any time:

The opening handshake

http
GET /lots/42 HTTP/1.1
Host: auction.example
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After the 101, messages travel as small frames of text or binary, with a few bytes of framing instead of HTTP headers. ws:// uses port 80 and wss://, the encrypted form you should always use, uses 443, the same ports as ordinary web traffic. Frames from the browser are masked, a safeguard for proxies that don't understand the protocol. Either side can send a ping, which the other must answer with a pong, to check that the connection is alive. A WebSocket can also run inside one stream of an HTTP/2 connection.

Technique

Direction

Cost per update

Best for

Short polling

Browser asks

A full request, usually empty

Rare updates where minutes of delay are fine

Long polling

Browser asks, server answers when ready

A full request per update

Fallback where streaming is blocked

Server-sent events

Server to browser

A few bytes on an open response

Feeds, scores, notifications, progress

WebSocket

Both ways

A few bytes on an open connection

Chat, bidding, games, collaborative editing

5 · Scaling

Open connections make servers stateful again

Each WebSocket or SSE stream lives on exactly one server for as long as it stays open. That is the very thing Stateless services & sessions warns about: a server now holds something that matters, its connected users. The auction runs 4 connection servers with about 5,000 watchers each. A bid placed by a bidder on server 1 must reach watchers on all four.

Bid serviceaccepts bid 19Pub/sub channeltopic lot-42Connection servers4 × 5,000 watcherspublish onceto each subscriberBid serviceaccepts bid 19Pub/sub channeltopic lot-42Connection servers4 × 5,000 watcherspublish onceto each subscriber

Each connection server subscribes to the lots its users are watching, and pushes each message down its own open connections.

The usual design splits the work in two:

  • Connection servers (often called gateways) only hold connections, track which user wants which topic, and push messages down. They contain no business logic, so they can be deployed rarely.
  • A pub/sub layer carries each message to every connection server with an interested user (Message queues & pub/sub). Ordinary stateless services publish into it.

Slack's real-time messaging works this way: gateway servers in several regions hold the WebSocket connections, and channel servers, chosen by consistent hashing, send each message to every gateway with a subscriber; messages reach clients worldwide in about 500 ms.

Big audiences multiply the work. A bid on lot 42 becomes 20,000 frames; a busy chat room or a famous account's post becomes millions. The same trade-offs as feeds apply (Fan-out on write vs read).

6 · The catch

Long-lived connections fight timeouts, deploys and reconnect storms

Infrastructure built for short requests treats a quiet connection as a dead one:

  • Idle timeouts. Amazon's Application Load Balancer and nginx both close a connection that carries no data for 60 seconds by default. A quiet lot would silently lose all its watchers. Send a ping every 20 or 30 seconds, or raise the timeout.
  • Proxies. Upgrade and Connection are hop-by-hop headers: each proxy must be configured to pass them on, or the handshake fails.
  • Balancing. A balancer places each connection once. Servers added later get only new connections, so existing load stays where it is, the same problem sticky sessions have (Load balancing).

Deploys are the hardest part. Restarting a connection server with 50,000 open connections drops all of them, and every client reconnects. If they all retry at once, the remaining servers and the authentication service take a spike:

All retry at once
50,000 reconnects/s
Random delay over 30 s
1,667 reconnects/s

50,000 clients in the first second, against 50,000 ÷ 30 ≈ 1,667 a second when each waits a random time of up to 30 seconds (Timeouts, retries, backoff & jitter).

So restart connection servers a few at a time, close connections gradually, and make clients wait a random time before reconnecting. Then make the reconnect lossless: the client says which message it saw last (bid 18), and the server replays anything newer, from a short history it keeps per topic. Without that, every bid placed during the reconnect is simply missed.

7 · In practice

Pick the simplest channel that meets the delay

For the auction, the answer is mixed: watchers get server-sent events, bidders a WebSocket, and a long-polling fallback covers networks that block both. In general:

Need

Choose

Updates every few minutes, delay fine

Short polling

Server pushes, browser only listens

Server-sent events

Both sides send often, low delay

WebSockets

Streaming blocked by an old proxy

Long polling as a fallback

App in the background or closed

Mobile push notifications (Presence & push notifications)

A checklist:

  • Use wss:// and authenticate during the handshake, then authorize every message as you would every request.
  • Ping every 20 to 30 seconds and set every proxy's idle timeout above that.
  • Keep connection servers thin, and route messages to them through pub/sub.
  • Number messages per topic so clients can resume from the last one they saw.
  • Reconnect with random delay, and restart connection servers gradually.
  • Watch open connections per server as a capacity metric, alongside requests.

Check yourself

5 questions

1. 20,000 watchers poll every 2 seconds. How many requests a second does the server handle, and what is the average delay to see a new bid?
2. With long polling, two bids arrive 10 milliseconds apart. Why does the second one reach watchers noticeably later than the first?
3. A user opens the auction in 7 tabs, each with a server-sent events stream, over HTTP/1.1. What happens to the seventh tab?
4. Ana's WebSocket is connected to connection server 3. Her rival's bid is accepted by the bid service, which talks to connection server 1. How does Ana see it?
5. Your load balancer closes connections after 60 seconds without data. A quiet lot has had no bids for two minutes. What prevents watchers from being cut off?

Takeaways

Remember this

  • Short polling trades load for delay: watchers ÷ interval requests a second, for an average delay of half the interval.
  • Long polling answers when there is news but pays a full request per update and a three-trip gap between bursts.
  • Server-sent events stream one way over plain HTTP with automatic reconnect; WebSockets give a two-way channel with tiny frames.
  • Open connections pin users to servers, so route messages to the right servers through pub/sub and keep connection servers thin.
  • Ping through idle timeouts, reconnect with random delay, and resume from the last message seen so nothing is lost.