🔌 WebSockets

Start as an HTTP request, then hijack the TCP connection for a persistent, bidirectional, framed message channel.

The Problem It Solves

Scenario: a chat app. The server needs to push a new message the instant it arrives. Plain HTTP is request/response — the server can't speak until asked. Your only options are to poll repeatedly (wasteful, laggy) or hold a request open (fragile).

WebSockets give both sides a symmetric, always-open pipe over a single TCP connection. Either peer sends a message at any time, with almost no per-message overhead.

The Upgrade Handshake

A WebSocket connection begins as HTTP. The client sends a normal GET with an Upgrade header; the server answers 101 Switching Protocols and the same TCP connection stops speaking HTTP and starts speaking the WebSocket frame protocol.

sequenceDiagram participant C as Client participant S as Server C->>S: GET /chat HTTP/1.1
Upgrade: websocket
Sec-WebSocket-Key: dGhl... S->>C: 101 Switching Protocols
Sec-WebSocket-Accept: s3pP... Note over C,S: Same TCP socket, now full-duplex frames C->>S: frame: "hello" S->>C: frame: "hi there" S->>C: frame: "someone joined"
# The client's opening request (over TCP, before upgrade)
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

# The server's response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The Sec-WebSocket-Accept handshake: the server concatenates the client's key with a fixed GUID, SHA-1 hashes it, and base64s the result. It proves the server actually understood the WebSocket protocol (not a confused HTTP cache), and stops cross-protocol attacks.

Frames vs Messages

After upgrade, data moves in frames. A logical message is one or more frames. Each frame header carries an opcode, a mask bit, and a length.

OpcodeMeaning
0x0Continuation (part of a fragmented message)
0x1Text frame (UTF-8)
0x2Binary frame
0x8Close
0x9 / 0xAPing / Pong (keepalive)

Ping/Pong Keepalive

Idle connections get silently dropped by NATs, load balancers, and proxies. The ping/pong control frames keep the path warm and let each side detect a dead peer.

A ping isn't a heartbeat you invent — it's built in. Send a 0x9 ping; a conforming peer must reply with a 0xA pong echoing the payload. No pong within your timeout ⇒ treat the connection as dead and reconnect. Most server libraries ping automatically on an interval.

A Client, End to End

const ws = new WebSocket("wss://example.com/chat", ["chat.v2"]); // subprotocol

ws.onopen = () => ws.send(JSON.stringify({ type: "join", room: "general" }));

ws.onmessage = (ev) => {
  const msg = JSON.parse(ev.data);   // one event == one message (not one frame)
  render(msg);
};

ws.onclose = (ev) => {
  // 1000 = normal, 1006 = abnormal (no close frame — often a dropped connection)
  if (ev.code !== 1000) scheduleReconnect();
};

ws.onerror = () => ws.close();
Subprotocols (the second WebSocket arg) let client and server agree on a message grammar during the handshake via Sec-WebSocket-Protocol — e.g. graphql-ws, mqtt, or your own chat.v2. The server echoes the one it accepts.

When To Use — and Not

Use WebSockets when…Prefer plain HTTP / SSE when…
Both sides send frequently (chat, collab editing, games)Only the server pushes (feeds, notifications) → SSE
You need low per-message overhead on a hot channelRequests are occasional → normal request/response
Sub-second bidirectional latency mattersYou want HTTP caching, proxies, and auth "for free"
The hidden costs: WebSockets bypass most HTTP infrastructure — caching, many WAFs, and request-scoped auth don't apply. You own reconnection, backpressure, heartbeats, and horizontal fan-out (a message for user X may land on a server that doesn't hold X's socket → you need a pub/sub bus).

Relationship to HTTP/2 and /3

Classic WebSockets run over HTTP/1.1's Upgrade. Over HTTP/2 they ride inside a single stream via the extended CONNECT method, so they share one multiplexed connection with your other requests. The transport changed; the frame protocol above it didn't.

Key Takeaways

  • WebSockets start as an HTTP Upgrade, then reuse the TCP socket for full-duplex frames.
  • A message is one or more frames; boundaries are preserved for you.
  • Client frames are masked; ping/pong keeps the connection alive.
  • Great for bidirectional, chatty channels — overkill when only the server pushes (use SSE).
  • You inherit reconnection, backpressure, and fan-out as your problems.