Skip to content

WebSocket vs HTTP

A stock ticker that updates every second, a multiplayer game broadcasting every player's position, a live chat where a message from one user has to reach another user's screen within a few hundred milliseconds — none of these fit the request/response model this course has used for every protocol so far. HTTP works when the client knows what it wants and asks for it. It has no good answer for "tell me the moment something happens," because in plain HTTP the server can never speak first.

The problem: HTTP has no way for a server to speak first

An HTTP connection is client-initiated by design. The client opens a TCP connection, sends a request, and the server's only job is to answer that specific request. There's no frame, no message type, nothing in HTTP/1.1 or even HTTP/2 that lets a server push data onto a connection the client didn't just ask about. If a chat application needs to know about a new message the instant it arrives, and the client can't ask "has anything happened?" every time something might have happened, plain HTTP simply has no mechanism for that.

Before WebSocket existed, web developers worked around this with two hacks, both still worth knowing because you'll find them in older systems and occasionally still choose one on purpose:

Short polling — the client just asks repeatedly. A setInterval in the browser fires a GET /messages request every two seconds, forever, whether or not anything changed.

Client -> GET /messages           Server -> 200 OK, []
   (2s later)
Client -> GET /messages           Server -> 200 OK, []
   (2s later)
Client -> GET /messages           Server -> 200 OK, [{"from": "alice", "text": "hi"}]

Every one of those requests pays for a full TCP connection reuse or setup, HTTP headers, and a round trip — most of which return nothing, because most two-second windows have no new message. Turn the interval down to catch messages faster and the wasted request volume climbs in direct proportion; turn it up to save bandwidth and real messages sit unseen for longer.

Long polling patches the worst of that: the client sends a request, and the server simply doesn't respond until it actually has something to say (or a timeout passes and it responds empty, at which point the client immediately opens another one). This cuts the empty-response waste dramatically, but it still opens a fresh HTTP request for every single message delivered, and the server has to hold a connection — and whatever memory or thread that connection consumes — open and idle for as long as the client is waiting.

Both hacks share the same root limitation: they're built on top of a protocol whose entire shape is "one request, one response, then the connection is free to be reused for something else." Neither actually gives the server the ability to just send data when it has data. WebSocket exists to remove the workaround by fixing the actual gap.

The upgrade: one HTTP request that becomes something else

A WebSocket connection is set up through a genuinely clever piece of protocol design, standardized as RFC 6455: it starts as an ordinary HTTP/1.1 request and then changes what the same TCP connection is used for, without opening a new one. This matters practically — it means a WebSocket handshake looks exactly like a web request to firewalls, load balancers, and proxies that only understand HTTP, so it can cross the same infrastructure a normal HTTP request would.

The client sends a normal-looking GET request carrying a specific set of headers:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Upgrade: websocket and Connection: Upgrade are the actual request — everything else the client wants is "please stop treating this connection as HTTP after this." Sec-WebSocket-Key is a random, base64-encoded nonce the client generates fresh for every handshake; its only purpose is proving to the client that the server which responds actually understood and processed this specific WebSocket request, rather than a cache or a misconfigured proxy just echoing something back.

If the server supports WebSocket on that path, it responds not with 200 OK but with:

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

Status 101 Switching Protocols is the whole mechanism in one status code: it tells the client "I'm honoring your upgrade request, and everything sent on this TCP connection from this point forward is no longer HTTP." Sec-WebSocket-Accept is computed by taking the client's key, appending a fixed GUID defined in the RFC, and SHA-1-hashing the result — a value only a server that actually speaks the WebSocket protocol could produce, which is what stops a plain HTTP server or an oblivious proxy from accidentally claiming to support an upgrade it doesn't understand.

After that single exchange, the TCP connection stays open, and both sides now read and write WebSocket frames on it instead of HTTP requests. There's no repeated handshake, no repeated headers, no new connection per message — the setup cost is paid exactly once, for the entire lifetime of the conversation.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: GET /chat HTTP/1.1 (Upgrade: websocket)
    S->>C: 101 Switching Protocols
    Note over C,S: Same TCP connection, now speaking WebSocket frames
    S->>C: frame: {"from": "alice", "text": "hi"}
    C->>S: frame: {"type": "typing"}
    S->>C: frame: {"from": "bob", "text": "hey"}

Frames, not requests

Once upgraded, WebSocket doesn't send anything resembling an HTTP message. It sends frames — a compact binary structure with a small header (a few bytes stating whether the payload is text or binary, how long it is, and whether the message continues in a following frame) followed by the payload itself. There's no method, no headers, no status line, no Host. A frame's overhead is a handful of bytes, versus the few hundred bytes of headers a typical HTTP request carries.

That difference is the actual point of the whole design, not just a minor optimization. A chat application sending one short message every few seconds barely notices HTTP's per-request overhead. A multiplayer game broadcasting position updates twenty times a second, or a trading platform pushing price ticks continuously, would spend more bytes on HTTP headers than on the data itself if it tried to do this with individual HTTP requests. WebSocket frames let either side send a message at any moment — client to server, server to client, in either order, with no request preceding the response — because after the upgrade there's no more request/response structure to follow at all.

Note

WebSocket is full-duplex: both directions are open simultaneously and independently, not "the server can push once it's been asked." A server can send three messages in a row with no client message in between, something that has no equivalent at all in the HTTP model.

Where WebSocket fits, and where it doesn't

WebSocket's real trade-off is state. An HTTP server handling ordinary requests can be close to stateless — any server behind a load balancer can usually answer any request, because nothing about the connection itself carries meaning. A WebSocket connection is different: it's a specific, long-lived, stateful link between one client and one specific server process, and the message meant for that client has to reach that exact process. Scaling WebSocket servers horizontally means either routing a client's reconnects back to the server holding its state, or introducing a shared message bus (Redis pub/sub and similar tools are common here) so any server instance can publish a message and have it delivered to whichever instance actually holds that client's connection. This is a meaningfully bigger operational commitment than a stateless HTTP API, and it's worth choosing WebSocket deliberately rather than reaching for it by default.

It's also asymmetric in a way that's easy to miss: WebSocket is equally good at client-to-server and server-to-client traffic, because both directions are genuinely symmetric frames on the same connection. If a system only ever needs the server to push data — status updates, live scores, log tails — and the client never needs to talk back on the same channel, that asymmetry is exactly what the next article's protocol was built around instead.

Practical scenario: a load balancer terminating idle connections

A team deploys a WebSocket-based chat feature behind an AWS Application Load Balancer and finds that connections silently drop after almost exactly 60 seconds of no messages being sent, even though both the browser and the backend process are still alive and never logged an error.

The investigation starts by checking the load balancer's own idle timeout setting, since a connection disappearing at a suspiciously round number is rarely the application's fault:

aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn <load-balancer-arn> \
  --query "Attributes[?Key=='idle_timeout.timeout_seconds']"
[
    {
        "Key": "idle_timeout.timeout_seconds",
        "Value": "60"
    }
]

That confirms it: the load balancer, like most reverse proxies and load balancers, closes any TCP connection — including an upgraded WebSocket one — that carries no traffic for longer than its configured idle timeout, because from the load balancer's point of view a silent connection is indistinguishable from an abandoned one. WebSocket's own protocol anticipates exactly this problem: it defines ping and pong control frames, tiny frames either side can send purely to prove the connection is still alive, with no payload the application needs to process. The fix is to have the server (or a library handling the WebSocket connection) send a ping frame every 30 seconds — comfortably under the load balancer's 60-second timeout — and treat a missing pong as a sign the connection is actually dead and should be cleaned up.

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn <load-balancer-arn> \
  --attributes Key=idle_timeout.timeout_seconds,Value=300

Raising the timeout, as shown above, buys more headroom, but it doesn't remove the underlying requirement — any intermediary between client and server can enforce its own idle limit, so an application that depends on a long-lived connection staying open needs to keep it demonstrably active on its own, rather than relying on a single infrastructure setting staying generous forever.

Practice exercises

  1. A colleague suggests replacing a 2-second short-polling loop with WebSocket for a dashboard that only ever needs to display server-pushed updates and never sends anything back except an initial subscription message. Using the state and scaling trade-off described above, is that a good fit, or does the traffic pattern point toward something else?
  2. Using the handshake headers shown above, explain specifically what stops a caching proxy that doesn't understand WebSocket from returning a stale, cached 200 OK response to a WebSocket upgrade request instead of passing it through.
  3. A WebSocket server behind two load-balanced backend instances needs to broadcast a chat message to every connected client, but each client's TCP connection terminates on whichever specific instance it happened to connect to. Sketch, at a high level, what has to exist between the two instances for a message received by instance A to reach a client connected to instance B.

WebSocket gives both sides an open, symmetric channel — either one can send at any time. But plenty of real systems only need traffic in one direction: a server continuously informing a client, with the client never talking back on that channel at all. Building a bidirectional, stateful connection for a problem that's actually one-directional is more machinery than the job needs, and the next article covers the protocol built specifically for that narrower, more common case.

Sources