WebSocket Framing#
WebSocket starts as an HTTP request with an Upgrade: websocket header. After the server agrees, the same TCP connection switches to bidirectional binary or text frames.
sequenceDiagram
participant C as Client (browser)
participant S as Server
C->>S: GET / HTTP/1.1\nUpgrade: websocket\nSec-WebSocket-Key: ...
S->>C: HTTP/1.1 101 Switching Protocols\nSec-WebSocket-Accept: ...
Note over C,S: TCP connection now full-duplex frames
C->>S: frame {opcode=text, payload=hello}
S->>C: frame {opcode=text, payload=hi}
C->>S: frame {opcode=binary, payload=...}
After the handshake, the same connection carries small framed messages in both directions with very low overhead - the foundation for chat, collaborative editing, live dashboards, gaming, and trading.
WebSocket (RFC 6455) is a thin framing layer over TCP that adds bidirectional messaging to a connection that started life as HTTP. Once upgraded, neither side needs to follow request-response semantics.
The handshake#
# Client request
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat.v1
# Server response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat.v1
Sec-WebSocket-Accept proves the server understood the WebSocket spec (it's a derived hash of the client's key + magic GUID).
Frame structure#
0 1 2 3
+-+-+-+-+-------+-+-------------+-------------------------------+
|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| | |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key (if MASK set) | Payload Data |
+-----------------------------------------------+ |
- FIN: final frame of a message (allows fragmentation)
- Opcode: text (0x1), binary (0x2), close (0x8), ping (0x9), pong (0xA)
- Mask: client→server frames MUST be XOR-masked (against cache-poisoning attacks)
- Payload length: 7 / 16 / 64 bits depending on size
Fragmentation#
A logical message can be split across many frames:
frame1: FIN=0 opcode=text payload="Hello "
frame2: FIN=0 opcode=continuation payload="from "
frame3: FIN=1 opcode=continuation payload="WS"
Useful for streaming large messages or interleaving control frames.
Ping/pong keepalive#
sequenceDiagram
participant C as Client
participant S as Server
loop every 30s
S->>C: PING
C->>S: PONG
end
Detects half-open connections (TCP keep-alive isn't always reliable behind NAT). If no pong within a window, close.
Subprotocols#
Sec-WebSocket-Protocol: chat.v1, mqtt, wamp declares which application-level format the frames will carry. Server picks one. Common subprotocols: MQTT (IoT), STOMP (messaging), WAMP (RPC + pub/sub), GraphQL-WS.
Scaling WebSockets#
flowchart TB
C1[Client A] --> LB[L7 LB<br/>sticky]
C2[Client B] --> LB
C3[Client C] --> LB
LB -->|sticky| WS1[WS server 1]
LB -->|sticky| WS2[WS server 2]
WS1 --- Bus[(Redis pub/sub<br/>NATS<br/>Kafka)]
WS2 --- Bus
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
class LB edge;
class WS1,WS2 service;
class Bus queue;
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
- Sticky sessions required during connection lifetime; can't bounce between servers.
- Pub/sub backplane (Redis, NATS, Kafka) lets servers broadcast across sessions on different hosts.
- Connection density: a single Node.js / Go server can hold 50k-1M concurrent connections; tune kernel limits (
ulimit -n,somaxconn). - Backpressure: slow consumer = OOM if you keep buffering. Use bounded outbound queues per connection.
WebSocket over HTTP/2 and HTTP/3#
RFC 8441 defines WebSocket over HTTP/2 using the :protocol pseudo-header. Browsers and modern servers support it; benefits include shared connection with regular HTTP traffic.
RFC 9220 extends this to HTTP/3.
Alternatives#
| Use case | Alternative |
|---|---|
| Server-to-client only | Server-Sent Events (SSE) |
| Polling acceptable | Long polling |
| Public-facing event push | SSE or WebSocket |
| Internal RPC | gRPC bidi-streaming |
| Web real-time + interactive | WebSocket still wins |
Operational pitfalls#
- Idle timeouts: LBs disconnect idle WS after 60s by default; set to hours and use ping/pong.
- HTTP-aware LBs: some L7 LBs buffer the response, breaking framing. Use streaming mode.
- Proxies that strip Upgrade: CORS proxies, some enterprise outbound proxies. Test.
- Authorization on every message: the auth check happens at upgrade time; if you need per-message auth, embed a token in each message or revoke at the session level.
Where WebSocket fits#
flowchart TB
WS((WebSocket))
RT[Realtime protocols<br/>parent overview]
TCP[TCP deep dive<br/>transport]
SS[Sticky sessions<br/>required for lifetime]
PS[Pub/Sub pattern<br/>backplane for fan-out]
RT --> WS
TCP --> WS
WS --> SS
WS --> PS
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class WS service;
class RT,TCP,SS,PS datastore;
Glossary & fundamentals#
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Realtime protocols | overview of WS/SSE/long-poll | realtime-protocols |
HLD |
TCP deep dive | the transport | tcp-deep-dive |
HLD |
Sticky sessions | required for WS scaling | sticky-sessions |
HLD |
Pub Sub Pattern | the fan-out substrate | pub-sub-pattern |
Quick reference#
Handshake#
HTTP GET with Upgrade: websocket; server responds 101 Switching Protocols.
Frame#
- FIN bit + opcode (text/binary/close/ping/pong)
- Mask bit (client → server required)
- Length: 7 / 16 / 64 bits
- Optional masking key
- Payload
Opcodes#
0x1 text · 0x2 binary · 0x8 close · 0x9 ping · 0xA pong
Keepalive#
Server-initiated PING every 30s; client must PONG.
Subprotocols#
Sec-WebSocket-Protocol: MQTT, STOMP, WAMP, GraphQL-WS.
Scaling#
- Sticky sessions required
- Pub/sub backplane (Redis, NATS, Kafka) for cross-server fanout
- Tune
ulimit -n,somaxconn - Bound per-connection outbound buffer
HTTP/2 + HTTP/3 variants#
- RFC 8441 (WS over h2)
- RFC 9220 (WS over h3)
Pitfalls#
- LB idle timeouts (set to hours)
- Auth only at upgrade (no per-message)
- HTTP-aware proxies that buffer responses
- Memory pressure from slow consumers
Alternatives#
- SSE for server-to-client only
- gRPC bidi streaming for internal
- Long polling for legacy compat
Refs#
- RFC 6455
- MDN - WebSocket server guide
- Phoenix Channels docs
FAQ#
How does the WebSocket handshake work?#
The client sends an HTTP GET with Upgrade: websocket and a key. The server replies 101 Switching Protocols with the hashed accept key, after which the TCP socket carries frames.
WebSocket vs HTTP: when should I use each?#
Use WebSocket for bidirectional, low-overhead, long-lived sessions like chat, live dashboards, or trading. Use HTTP for stateless request-response or REST APIs.
WebSocket vs Server-Sent Events?#
WebSocket is bidirectional, binary or text, over one TCP socket. SSE is server-to-client text over HTTP. SSE auto-reconnects and is simpler when you only push.
How do you scale WebSockets to millions of connections?#
Shard by user ID across a fleet of gateway servers, use a pub/sub bus like Redis or Kafka for fan-out across nodes, and offload TLS to a layer-4 load balancer.
What is a WebSocket frame?#
A small binary header with opcode, payload length, and optional mask, followed by the payload. Frames can be text, binary, ping, pong, or close, and may be fragmented.
Related Topics#
- Realtime Protocols: the overview of WS vs SSE vs polling
- Sticky Sessions: WebSockets inherently need stickiness
- Pub Sub Pattern: the backplane that fans out across WS servers