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:
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
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.
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.
UpgradeandConnectionare 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:
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
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.