HTTP has one rule and it is absolute: the client requests and the server responds. A server that has updates has no way to say so. It has to wait to be requested that info.
Every real-time app or feature you have ever used is a workaround for that one rule.
What were we doing before?
Requesting, repeatedly. The client sets a timer and requests the same URL every couple of seconds, and almost every one of those requests comes back empty. You pay full HTTP price — request line, host, cookies, user agent, accept headers, perhaps a kilobyte of them — to be told nothing has changed.
Then someone works out that the server can simply not answer until it has something to say, which is long polling, and that a response body can be left open and dripped into, which is server-sent events. Both are genuinely useful. Both are still shaped like a request.
Four ways to hear about something that happened on the server
- Connections
- 1 reused (or 5 new)
- HTTP requests
- 5 in 10 seconds
- Per-message overhead
- ≈7 KB of headers and cookies for 2 useful messages
- Delay on the 3.2 s event
- up to 2.0 s — the poll interval
Simple, works everywhere, and wrong on both axes at once: it is too slow when nothing happens and too chatty when nothing happens.
Notice what changes between the first row and the last. It is not really speed — long polling delivers the 3.2 second event about as fast as a WebSocket does. What changes is that the client stops having to ask, in both directions, and the per-message tax falls from kilobytes to bytes.
What is a WebSocket, mechanically?
A WebSocket is a TCP connection that starts life as an HTTP request and then stops being HTTP.
That design choice is the whole reason the protocol succeeded. A brand new protocol on
a brand new port would have been blocked by every corporate firewall and confused every
proxy in the path. Instead, RFC 6455
opens with a perfectly ordinary GET carrying an Upgrade header, over port 80 or
443, wrapped in the same TLS as everything else if the URL says wss://. The
infrastructure between you and the server sees a request it recognizes.
The opening handshake, step by step
1 — The client sends an ordinary HTTP/1.1 GET
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.comNothing exotic yet. This travels over the connection the browser already opened, and any HTTP proxy in the path sees a request it understands. Sec-WebSocket-Key is 16 random bytes, base64-encoded — a nonce, not a secret.
2 — The server proves it understood
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Accept is SHA-1 of the client key concatenated with the fixed GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, base64-encoded. It is not authentication and never was — it exists so a cache or a naive server cannot accidentally satisfy the request with a stored response.
3 — The connection stops being HTTP
<-- same TCP connection, new protocol -->
client → server frame: FIN=1 opcode=0x1 masked len=5 "hello"
server → client frame: FIN=1 opcode=0x1 masked=0 len=5 "world"After the 101 there is no request, no response and no headers. Both sides send frames whenever they have something to say, on the connection that was already open and already TLS-protected if the URL was wss://.
4 — Closing is also a frame
client → server frame: opcode=0x8 code=1000 "going away"
server → client frame: opcode=0x8 code=1000
<-- TCP FIN -->A close frame carries a status code from the same registry as HTTP status codes in spirit — 1000 normal, 1001 going away, 1006 abnormal, which is the one you will see in production logs when a mobile network drops out mid-conversation.
After the 101 Switching Protocols, both sides drop HTTP framing and start sending
WebSocket frames. There is no request. There is no response. There is no correlation
between what one side sends and what the other sends back — if you want request and
response semantics on top, you build them yourself, and most production systems do.
What does a message cost?
Almost nothing, which is the second reason to use this protocol.
What a message actually costs on the wire
Frame layout
81 85 3a 7f 9c 21 52 1a f0 4d 55
Five bytes of message, six bytes of frame. The equivalent HTTP request would carry request line, host, cookies, user agent and accept headers before the payload.
Two bytes of header for a short message, plus four bytes of mask key on anything the browser sends. Compare that with the several hundred bytes of headers on every HTTP request and the arithmetic for a chat application, a live cursor, or a market data feed stops being close.
While WebSocket masking seems like a fake security measure, it has a very specific job. The browser XORs outbound data with a random four-byte key and sends that key in the clear, so it doesn’t hide anything from network snoops. Instead, it prevents cache poisoning. By randomizing the payload bytes, it stops older caching proxies from misinterpreting a malicious payload as a second HTTP request.
Didn’t HTTP/2 make all this unnecessary?
This is the most common misconception in the area, so it is worth being precise.
HTTP/2 (RFC 9113) introduced multiplexing: many concurrent streams over one TCP connection, with binary framing and header compression. It fixed the problem where a browser opened six connections per origin and queued the seventh asset behind them.
Eight assets, two protocols, and what one lost packet does to each
HTTP/1.1 — six connections
last byte at 350 ms
HTTP/2 — one connection, eight streams
last byte at 285 ms
On a clean link, multiplexing wins outright: one handshake instead of six, and no asset waiting for a free connection.
What it did not do is change the rule at the top of this article. HTTP/2 streams are still request/response. Server push existed, was almost universally misused, and has been removed from Chrome. If the server wants to speak first, HTTP/2 gives it nothing that HTTP/1.1 did not.
The toggle above reveals a subtle but important catch - multiplexing didn’t eliminate head-of-line blocking, it just shifted it to a different layer. With HTTP/1.1’s six parallel connections, a dropped packet only delayed one-sixth of your resources. However, over HTTP/2’s single connection, a single lost packet stalls everything. This happens because TCP won’t deliver any new data to the application until the missing packet is recovered. This exact bottleneck is why HTTP/3 abandons TCP in favor of QUIC, which manages streams independently so a single packet loss only affects its specific stream.
For completeness: HTTP/2 does have a WebSocket bootstrap of its own
(RFC 8441) using an extended CONNECT,
which lets a WebSocket share a multiplexed connection with regular requests. Browsers
implement it, servers vary, and the API you write against does not change.
What should you actually build with it?
The honest test is whether the server has something to say that the client did not ask for, often enough that asking would be wasteful.
Good fits. Chat and presence. Collaborative editing, where every keystroke is an operation both directions. Live dashboards and trading screens. Multiplayer game state. Notifications. Streaming a language model’s tokens as they are generated — though this one is more often server-sent events, since the traffic is one-directional.
Poor fits. A form submission. A page load. Anything where you would otherwise make one request and get one response. Wrapping a normal API in a WebSocket buys you nothing and costs you HTTP caching, HTTP status codes, standard retries, and every proxy and observability tool that understands requests.
The middle. A notification bell that updates a few times an hour does not need a persistent connection per user. Server-sent events over HTTP/2 will do it for a fraction of the operational cost, and the browser reconnects for you.
What breaks in production?
Everything on this list has cost somebody a weekend.
Connections die silently. A mobile network changes cell, a laptop lid closes, a NAT table entry expires. TCP does not notice for minutes. Both ends think they are connected and neither is. The fix is application-level heartbeats, send a ping frame every 30 seconds, and treat a missing pong as a dead connection. TCP keepalive is unreliable here, its default timeout is measured in hours.
Proxies and load balancers idle you out. Most default to around 60 seconds of inactivity. Your heartbeat needs to be shorter than the shortest idle timeout anywhere in the path, including ones you do not control.
Reconnection storms. A server restarts and every client reconnects in the same second, which knocks the server over, which produces another reconnection storm. Exponential backoff with jitter is not optional at any scale worth having.
Messages get lost across a reconnect. A WebSocket gives you ordered delivery within a connection and no guarantee whatsoever across one. If a message matters, give it a sequence number, have the client send its last-seen number on reconnect, and replay from there. Otherwise the user silently misses events, which is worse than an error.
Sticky state. A WebSocket is a long-lived connection to one specific process, which makes horizontal scaling harder than it is for stateless HTTP. You need either sticky routing or a shared backplane — Redis pub/sub, NATS, a message broker — so a message published on one server reaches subscribers held on another.
No status codes. There is no 404, no 401 and no 429 once the connection is open. You design your own error envelope, and your monitoring will not understand it unless you make it.
Backpressure. A slow client, or one that stops reading, causes the send buffer to grow until the server runs out of memory. Bound the queue per connection and drop or disconnect when it is exceeded. This is the failure that takes out a whole node rather than one user.
Authentication is awkward. The browser WebSocket constructor cannot set headers,
so there is no Authorization on the upgrade request. The practical options are a
cookie (works, but think about cross-site request forgery, and check the Origin
header server-side), a short-lived token in the query string (it will end up in access
logs), or connecting unauthenticated and sending credentials as the first message,
which is the cleanest of the three and the one to prefer.
What are the ways around the limits?
Most of them are the same handful of moves, applied consistently.
- Heartbeat and idle timeouts — ping every 20–30 seconds, drop after two missed pongs. Shorter than every proxy in the path.
- Reconnect with backoff and jitter — start at a second, cap at thirty, randomize.
- Sequence numbers and replay — the client tells you where it got to; you send what it missed. This makes reconnection a non-event rather than a bug report.
- A backplane for fan-out — Redis, NATS or a broker between server processes, so a message reaches every connection regardless of which node holds it.
- Bounded per-connection queues — with an explicit policy: drop oldest, drop newest, or disconnect. Choose deliberately, because the default is “run out of memory”.
- Fall back to server-sent events — for one-directional streams, especially where a hostile network or an old proxy is in play. SSE reconnects itself and rides on plain HTTP.
- Let a managed layer hold the connections — Cloudflare Durable Objects, Ably, Pusher, PubNub, or a Phoenix or ActionCable server. The hard part of real-time is connection lifecycle at scale, not the protocol.
The short version
WebSockets exist because HTTP cannot let a server speak first. They cost one handshake and a few bytes per message thereafter, in both directions, on the same port and the same TLS as the rest of your traffic. HTTP/2 and HTTP/3 made ordinary requests much faster and left that gap exactly where it was.
Use one when the server genuinely has news. Use server-sent events when the news only travels one way. Use a plain request when you are asking a question — and when you do open a socket, budget for heartbeats, reconnection, replay and backpressure on the first day, because every one of them will find you eventually.