backend / protocols / websockets / 01_websocket_protocol.md

WebSocket Protocol

7 interview angles 7 min read source

WebSocket Protocol

RFC 6455. Starts as an HTTP/1.1 request with an Upgrade: websocket header; after the handshake, the same TCP connection carries WebSocket frames in both directions until either side closes. The protocol itself is minimal — frame structure, control frames, close codes.

The handshake

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat.v1, chat.v2
Origin: https://example.com

Server response on accept:

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

After the 101, the connection switches protocols. No more HTTP — both sides now send and receive WebSocket frames.

Key headers:

Header Means
Upgrade: websocket client wants to switch protocols
Connection: Upgrade this is an upgrade-style request
Sec-WebSocket-Key client-chosen random base64 nonce
Sec-WebSocket-Accept server hashes the key + magic GUID with SHA1; proves it’s a real WebSocket server, not a confused HTTP endpoint
Sec-WebSocket-Version always 13 for RFC 6455
Sec-WebSocket-Protocol optional subprotocol negotiation (e.g., mqtt, wamp.2.json, custom)
Sec-WebSocket-Extensions extensions like permessage-deflate (compression)
Origin the requesting origin — server should validate to prevent cross-origin attacks

The Sec-WebSocket-Accept math:

hash = SHA1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
accept = base64(hash)

The magic GUID is hardcoded in the spec. The client verifies the server hashed the key correctly. Prevents accidentally talking to an unrelated HTTP server.

Frame structure

Once upgraded, data flows as frames. Simplified layout:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+-------------------------------+-------------------------------+
|     Masking-key (4 bytes, client→server only)                 |
+---------------------------------------------------------------+
:                     Payload Data                              :
+---------------------------------------------------------------+

Per-frame bits:

Bit Meaning
FIN last frame in a message (1) or more coming (0 — fragmentation)
RSV1-3 extension bits (e.g., permessage-deflate uses RSV1)
opcode frame type (see below)
MASK masked? Client→server frames MUST be masked; server→client MUST NOT
payload len 7-bit length, with extended forms for larger

Opcodes (frame types)

Opcode Name Use
0x0 Continuation next part of a fragmented message
0x1 Text UTF-8 text payload
0x2 Binary raw bytes
0x3-0x7 reserved non-control
0x8 Close initiate connection close
0x9 Ping keepalive / health check
0xA Pong response to ping
0xB-0xF reserved control

Control frames (0x8, 0x9, 0xA) MUST be ≤ 125 bytes and not fragmented. Used for connection management, not data.

Most libraries hide opcodes — you call ws.send_text(...) or ws.send_bytes(...) and the library picks the opcode.

Why client→server frames are masked

The masking-key XORs the payload before sending. The server XORs back. This is not for security (the key is in the frame); it’s a defense against ancient HTTP proxies that might cache or interpret WebSocket bytes as HTTP responses.

Implication: client → server is more CPU-intensive than server → client. Negligible for normal payloads.

Fragmentation

A single message can span multiple frames. FIN=0 means more frames follow; FIN=1 is the last.

Frame 1: opcode=0x1 (Text), FIN=0, payload="Hello "
Frame 2: opcode=0x0 (Continuation), FIN=0, payload="from "
Frame 3: opcode=0x0 (Continuation), FIN=1, payload="server"

Used for streaming large messages without buffering. Most apps don’t fragment intentionally; the library handles it.

Control frames (ping, pong, close) can be interleaved between data frame fragments. So a long message doesn’t block pings.

Close handshake

Either side initiates by sending a close frame (opcode 0x8) with a 2-byte status code + optional reason:

Side A: Close(1000, "going away")  →
Side B:                              ← Close(1000, "")
both close the TCP connection

Standard close codes (RFC 6455 §7.4):

Code Meaning
1000 Normal closure
1001 Going away (browser navigating away, server shutting down)
1002 Protocol error
1003 Unsupported data (e.g., text received but server only handles binary)
1005 (reserved — no status received)
1006 (reserved — abnormal closure, no close frame)
1007 Invalid frame payload data (e.g., non-UTF-8 in text frame)
1008 Policy violation
1009 Message too big
1010 Mandatory extension (client asked for extensions server didn’t provide)
1011 Internal server error
1015 (reserved — TLS handshake failure)
3000-3999 Reserved for libraries / frameworks
4000-4999 Application-specific

For custom app errors, use 4000-4999. E.g., 4001 = “auth failed.”

URL schemes

ws://example.com/chat        # WebSocket over HTTP (port 80)
wss://example.com/chat       # WebSocket over HTTPS (port 443)

wss:// is HTTPS-style: TLS first, then HTTP upgrade, then WebSocket. Always use wss:// in production. Browsers refuse ws:// from HTTPS pages.

Extensions

Sec-WebSocket-Extensions lets client and server negotiate optional features. The main one in practice: permessage-deflate (per-message compression).

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Tradeoff: CPU cost for compression vs bandwidth savings. Good for chat / JSON; bad for already-compressed payloads (binary images, encrypted data).

Subprotocols

Sec-WebSocket-Protocol lets both sides agree on an application-level protocol on top of WebSocket:

Sec-WebSocket-Protocol: mqtt, stomp, wamp.2.json

The server picks one and responds with the chosen subprotocol. Useful when the same endpoint speaks multiple protocols.

In Python:

async def handler(websocket):
    subprotocol = websocket.subprotocol     # e.g., "mqtt"

HTTP/2 and HTTP/3

WebSockets defined over HTTP/1.1 originally. RFC 8441 added “WebSockets over HTTP/2” using CONNECT instead of Upgrade. RFC 9220 extended this to HTTP/3.

In practice: most servers and proxies still use HTTP/1.1 for WebSockets. HTTP/2 WebSocket support is uneven; HTTP/3 even rarer. For broad compatibility, HTTP/1.1.

What WebSocket is NOT

  • Not stateless. Connection persists; both sides keep state. See ../../07_rest_apis/09_stateful_vs_stateless.md.
  • Not message-bounded reliably. TCP delivers bytes; frame boundaries are the protocol’s job. Libraries hide this, but at the network level no message-boundary guarantee comes for free.
  • Not HTTP. Only the handshake is HTTP. After 101, the protocol bytes don’t follow HTTP semantics — caches, intermediaries, load balancers that don’t understand WebSocket break things.
  • Not a queue. No persistent message storage. If the client is offline, messages aren’t delivered later. Layer your own queue if needed.
  • Not load-balancer-friendly by default. Long-lived connections complicate routing; see 07_scaling.md.

Common pitfalls

  • No wss:// in production — sniffable plaintext WebSockets; firewalls / corporate proxies often block plain ws://.
  • Forgetting Origin checks — server accepts WebSocket connections from any origin = CSWSH (Cross-Site WebSocket Hijacking). See 06_security.md.
  • Treating large messages as atomic — a 100 MB message blocks other writes on the same socket. Stream large data with chunked app-level framing.
  • No close frame on shutdown — peer sees abnormal close (1006), can’t tell normal from network failure.
  • Mixing text and binary frames carelessly — clients expecting one type, server sends the other; opcodes differ.

Common interview confusions

  • “WebSocket is HTTP.” — only the handshake. After 101, no HTTP semantics apply.
  • “WebSocket replaces HTTP for everything.” — different tool. HTTP for request/response (caching, REST). WebSocket for push and bidirectional.
  • “The connection stays open without traffic.” — most intermediaries (firewalls, NATs, load balancers) drop idle connections after 30-300s. Heartbeats keep them alive. See 05_connection_management.md.

Interview angle

  • “How does the WebSocket handshake work?” — client sends HTTP/1.1 GET with Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key (random nonce), Sec-WebSocket-Version: 13. Server responds 101 Switching Protocols with Sec-WebSocket-Accept (SHA1 of key + magic GUID). After 101, the connection carries WebSocket frames.
  • “What’s a WebSocket frame?” — binary structure containing: FIN bit, opcode (text/binary/ping/pong/close), MASK bit, payload length, masking key (client→server only), payload. The basic unit of WebSocket communication.
  • “What are the WebSocket message types?” — text frames (UTF-8), binary frames, and control frames: ping, pong, close. Control frames are ≤125 bytes and used for connection management.
  • “What are common WebSocket close codes?” — 1000 normal, 1001 going away, 1002 protocol error, 1006 abnormal (no close frame received — typically network failure), 1008 policy violation, 1011 internal server error. Custom app codes 4000-4999.
  • “What’s the difference between ws:// and wss://?”wss:// is WebSocket over TLS (port 443). Always use in production. Plain ws:// is sniffable and blocked by HTTPS pages.
  • “Why are client frames masked?” — XOR-masking with a per-frame key. NOT for security; defends against old HTTP proxies that might cache or misinterpret WebSocket bytes. Server-sent frames are not masked.
  • “What’s Sec-WebSocket-Key for?” — random nonce from the client. Server hashes (SHA1) with a fixed GUID and returns as Sec-WebSocket-Accept. Proves the server is WebSocket-aware (not a confused HTTP server that 200s on any request).