Open a live sports score, a chat app, a collaborative document where you can see other people's cursors
moving, or a trading dashboard where prices tick in real time. In every case, the server has
news and needs to get it to your screen the instant it happens. But the web was not built for
that.
So how did the web learn to feel alive? The answer is a thirty-year arc of increasingly clever workarounds and, finally, a protocol built for the job: the WebSocket, a persistent, full-duplex, bidirectional channel that begins its life as an ordinary HTTP request and then transforms into a raw two-way pipe. This page walks that whole evolution — polling, long-polling, Server-Sent Events, and WebSockets — so you understand not just how real-time works but why each rung of the ladder exists.
HTTP's request/response shape is a wonderful fit for fetching pages: you ask for a URL, you get a document, done. Each exchange is independent and the connection can be closed and forgotten. But that same shape is exactly wrong for real-time updates, because the interesting event — "a new message arrived", "the price moved", "your teammate typed" — originates at the server, at a moment the client cannot predict. In pure HTTP, the client would have to somehow guess when to ask. The whole history of real-time web is a series of answers to one question:
how does the server get data to the client without the client having asked for that specific thing right now?
There are only two fundamental strategies, and everything else is a refinement of them:
Each technique fixes the flaw of the one before it. It is genuinely a ladder, and knowing the rungs is how you choose the right tool.
Accept: text/event-stream, and the server keeps it open
and writes a sequence of text events down it over time, server → client. Built into browsers as
EventSource, with automatic reconnection. Perfect for feeds — notifications, live scores,
a status ticker — but the client cannot send anything back on the same channel.
The clever part of WebSockets is how they start. A WebSocket does not bypass the web — it begins
as a perfectly ordinary HTTP GET that politely asks to be upgraded. The client sends
headers like these:
If the server agrees, it replies with a special status code — 101 Switching Protocols — and from that instant the same TCP connection stops speaking HTTP and becomes a raw WebSocket channel:
This is why WebSockets work through the existing web infrastructure — they ride ports 80/443 and start as HTTP, so firewalls and proxies let them through — yet escape HTTP's request/response straitjacket once upgraded. The handshake is the bridge from the old world to the new.
Once upgraded, the two sides no longer send HTTP requests — they exchange frames. A WebSocket frame is a lightweight envelope: a few bytes of header (an opcode saying what kind of frame it is, a length, and — from client to server — a masking key) wrapped around the payload. That is the source of the efficiency: where each long-poll message dragged a fat block of HTTP headers along, a WebSocket message adds only a handful of framing bytes.
The opcode tells the receiver how to treat the frame. The important ones:
| Opcode | Meaning |
|---|---|
0x1 text | a UTF-8 text message (e.g. JSON) |
0x2 binary | a binary message (e.g. an image chunk) |
0x8 close | begin a clean shutdown of the connection |
0x9 ping | heartbeat: "are you still there?" |
0xA pong | heartbeat reply: "yes, still here" |
The ping/pong pair matters more than it looks: a persistent connection that goes quiet is indistinguishable, to the network, from a dead one. Intermediaries (proxies, NAT boxes, load balancers) happily drop idle TCP connections after a minute or two. So both ends periodically ping and expect a pong; a missing pong means the peer is gone and the connection should be torn down and re-established. Keeping the pipe alive is your job, not magic.
Why does everyone reach for persistent connections? Overhead. The demo below simulates a 60-second window in which the server has 3 real pieces of news to deliver, and counts the network round-trips and the bytes of overhead each strategy spends to deliver them. The numbers make the case better than prose.
Short polling burns a huge, fixed cost whether or not anything happens; long polling scales with the message count but pays full HTTP headers each time; the WebSocket pays one handshake and then almost nothing per message — and the client can talk back on the same connection. For a chatty, interactive workload the persistent connection wins by a mile.
More power is not always the right answer; each tool fits a shape of problem.
| Use… | When… |
|---|---|
| Short/long polling | updates are rare, or you must support ancient clients and can't hold connections |
| Server-Sent Events | the flow is one-way, server → client: notifications, live scores, log tails, progress feeds |
| WebSockets | both sides talk continuously: chat, multiplayer games, collaborative editing, live cursors |
| WebRTC | you need direct peer-to-peer media (video/voice) or lowest-latency data, often bypassing the server |
| WebTransport (over QUIC/HTTP-3) | the newest option: multiplexed, unordered-capable streams and datagrams over UDP-based QUIC — like WebSockets but without head-of-line blocking |
SSE and WebSockets cover the vast majority of real-time web today. WebRTC is the choice when the payload is live audio/video and you want peers talking (nearly) directly to each other. WebTransport over QUIC is the emerging successor for cases where a single WebSocket's strict in-order TCP stream causes head-of-line blocking — QUIC's independent streams over UDP dodge it.
It is easy to picture a WebSocket as magic: the server just "pushes" and it appears, costing nothing. Three realities puncture that.
101 Switching Protocols), so it must pass through your
HTTP layer, auth and proxies first. It is not a separate magic transport bolted onto the browser — it
is HTTP that graduates into something else.
Plain HTTP is stateless, so any request can hit any server behind a load balancer — trivially horizontal. WebSockets break that: a connection is pinned to one specific server for its whole life, and that server holds the client's state. So when user A (connected to server 1) sends a chat message that must reach user B (connected to server 3), server 1 has no direct line to B. The standard fix is a pub/sub fan-out backend — a shared message bus (Redis pub/sub, Kafka, NATS, a managed service) that every app server subscribes to. Server 1 publishes the message to the bus; the bus delivers it to server 3; server 3 pushes it down B's socket. The persistent connections stay at the edge, and a horizontally-scalable message layer sits behind them doing the routing. Real-time at scale is less about the WebSocket itself and more about the fan-out plumbing behind it.