Skip to content

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.

Further reading#