ποΈ 06062026 1500
π #api #realtime #protocols
A persistent, full-duplex communication channel over a single TCP connection. Both client and server can send messages at any time without waiting for the other. Starts as HTTP, then upgrades.
The Upgrade Handshakeβ
WebSocket piggybacks on HTTP to get through firewalls and proxies, then switches protocol:
Client Server
β β
βββ GET / HTTP/1.1 β
β Upgrade: websocket β
β Connection: Upgrade β
β Sec-WebSocket-Key: dGhlIHN... β
β Sec-WebSocket-Version: 13 ββ>
β β
β<ββ HTTP/1.1 101 Switching Protocols β
β Upgrade: websocket β
β Connection: Upgrade β
β Sec-WebSocket-Accept: s3pP... ββ€
β β
β<========= WebSocket frames ==========>β
β (full-duplex from here) β
Sec-WebSocket-Key is a random Base64 nonce. Server concatenates it with a magic GUID, SHA-1 hashes, Base64-encodes β Sec-WebSocket-Accept. This proves the server understands WebSocket, not just any HTTP server reflecting headers.
Frame Typesβ
After upgrade, communication is via frames, not HTTP requests:
| Opcode | Type | Purpose |
|---|---|---|
0x1 | Text | UTF-8 text message |
0x2 | Binary | Raw bytes (images, protobuf, etc.) |
0x8 | Close | Initiate graceful shutdown (status code + reason) |
0x9 | Ping | Heartbeat probe (server or client) |
0xA | Pong | Response to ping |
Messages can be fragmented across multiple frames for streaming large payloads without buffering the whole thing in memory.
Client-to-server frames are always masked (XOR with a 4-byte key) β prevents cache poisoning attacks on intermediary proxies. Server-to-client frames are unmasked.
Connection Lifecycleβ
[HTTP Handshake] β [Open] β [Messages β] β [Close Handshake] β [Closed]
β
(close frame sent,
wait for close frame back)
Ping/pong keeps the connection alive through idle-timeout proxies and detects dead peers. Either side can send a ping; the other must respond with a pong containing the same payload.
Subprotocolsβ
The Sec-WebSocket-Protocol header negotiates an application-level protocol on top of WebSocket:
Client: Sec-WebSocket-Protocol: graphql-ws, json
Server: Sec-WebSocket-Protocol: graphql-ws
Common subprotocols: graphql-ws, wamp, stomp, mqtt. The WebSocket spec defines the framing; the subprotocol defines message semantics.
Browser APIβ
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.onclose = (e) => console.log(`closed: ${e.code} ${e.reason}`);
ws.onerror = (e) => console.error('ws error', e);
No built-in reconnection. Unlike server_sent_events, you must implement reconnect logic yourself (backoff, jitter, re-subscribe).
When to Useβ
- Bidirectional streaming. Chat, multiplayer games, collaborative editing, trading terminals.
- Low-latency. No HTTP overhead per message after the handshake. One TCP connection, minimal framing.
- Binary data. Native binary frames β protobuf, MessagePack, images β no Base64 encoding tax.
- High-frequency messages. Sub-100ms intervals where HTTP request overhead matters.
Common Pitfallsβ
Load balancer configuration. WebSocket connections are long-lived and stateful. Round-robin LBs break if a reconnect lands on a different backend. Solutions: sticky sessions (cookie/IP hash), or a pub/sub backbone (Redis, Kafka) so any backend can serve any client.
No built-in reconnection. Connection drops silently on network changes (WiFi β cellular). Implement heartbeat + reconnect with exponential backoff. Libraries like reconnecting-websocket (JS) handle this.
- Proxy/firewall issues. Some corporate proxies block the
Upgradeheader or don't understand WebSocket.wss://(TLS) usually gets through because proxies can't inspect the upgrade inside the encrypted tunnel. - No HTTP caching or compression. Each message is independent β no
ETag, nogzipcontent-encoding. Compress at the application layer (permessage-deflate extension exists but has CPU cost and mixed support). - Resource exhaustion. Each connection is a persistent socket. 100k concurrent users = 100k sockets. Monitor
file descriptorlimits, tune OSulimit, and use non-blocking I/O. - Security.
Originheader validation is critical β WebSocket doesn't enforce same-origin policy. Without it, any page can open a connection to your server.
Server Frameworksβ
| Language | Library/Framework |
|---|---|
| Java | Spring WebSocket (@ServerEndpoint), Netty |
| Node.js | ws, Socket.IO (adds fallback + rooms) |
| Go | gorilla/websocket, nhooyr.io/websocket |
| Python | websockets, FastAPI WebSocket |
Relatedβ
- server_sent_events β simpler alternative when only serverβclient push is needed.
- long_polling β fallback when WebSocket is blocked.
- realtime_patterns_comparison β decision guide across all approaches.
- load_balancer β sticky sessions for WebSocket connections.