WebSocket: A Protocol Where the Server Gets to Speak First
HTTP was built on a one-way model where the client asks and the server answers. For chat, notifications, and real-time sync (anything the server needs to push on its own initiative), that shape has been a bottleneck for as long as the web has needed to feel live. From the handshake through the frame format, WebSocket's role as a P2P signaling channel, and its relationship to the newer WebTransport, here is the bidirectional protocol in full.
Why it exists: the limits of the request/response model
The web's foundational request/response model leaves no way for the server to hand over new data the moment it appears; it has to wait for the client to ask again. For use cases like chat, live price updates, and notifications, where the server itself wants to initiate delivery, developers made do with polling, where the client asks again every few seconds, or long polling (Comet), where the server deliberately holds the response open until it has something to say. Both carry real costs: HTTP header overhead on every new connection, the churn of repeatedly opening and closing connections, and noticeable latency. WebSocket, standardized as RFC 6455 in 2011, was designed to solve this inefficiency at the root. It provides a full-duplex channel over a single TCP connection, where either the client or the server can send a message at any moment, independent of the other side.
Handshake and connection setup
A WebSocket connection does not start out speaking some exotic protocol; it begins life as an ordinary HTTP request. The client sends an HTTP/1.1 GET request carrying Upgrade: websocket and Connection: Upgrade headers, along with a Sec-WebSocket-Key, a random 16-byte value, base64-encoded. If the server agrees to switch protocols, it replies with status 101 Switching Protocols and a Sec-WebSocket-Accept header. That value is computed by concatenating the received key with a fixed GUID, 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, taking the SHA-1 hash, and base64-encoding the result. The client performs the same computation itself and checks it against what came back, confirming that the response really did come from a server that understands WebSocket. Once the handshake completes, HTTP semantics disappear from that TCP connection entirely, and all further traffic switches to the frame format described below. Two URL schemes are used: plain-text ws:// (default port 80) and TLS-protected wss:// (default port 443, with certificate validation just like HTTPS).
Frame structure: how messages get chopped up
After the handshake, WebSocket traffic is not sent as raw messages; it is broken into discrete frames with a fixed layout. A single logical message can also be split across multiple frames (fragmentation).
- FIN bit
- A single bit marking whether this frame is the final fragment of a message. When it is 0, more frames follow, and the receiver reassembles the original message by concatenating the fragments carrying the "continuation" opcode (0x0).
- Opcode
- A 4-bit field identifying the frame type. Besides the data frames, text (0x1, UTF-8-encoded payload) and binary (0x2), there are control frames: close (0x8) to end the connection, and ping (0x9) / pong (0xA) for keepalive.
- Payload length
- Encoded with a variable-length scheme: when the length does not fit in the base 7-bit field, the value 126 signals a 16-bit extended length and 127 signals a 64-bit extended length placed right after. This keeps overhead low for small messages while still accommodating large payloads.
- Masking
- Frames sent from client to server are required to XOR the entire payload with a 4-byte masking key (frames from server to client must not be masked). This is not about confidentiality; it is an RFC requirement meant to stop a malicious web page from crafting a payload that poisons the cache of a transparent proxy sitting on the path.
Keepalive, subprotocols, and the origin model
- Ping/pong: sending a ping frame (opcode 0x9) is expected to elicit a pong frame (0xA) carrying the same payload. No response signals a dead connection, and the periodic exchange itself also keeps proxies and NAT devices from timing out an idle TCP connection on their own.
- Subprotocols: during the handshake, the client lists candidates in a
Sec-WebSocket-Protocolheader, and the server picks and confirms one, letting both sides agree on an application-specific message format over the same WebSocket connection (for example, MQTT, a lightweight IoT messaging protocol, over WebSocket, orgraphql-wsfor GraphQL subscriptions). - Origin model: unlike XMLHttpRequest or fetch, WebSocket connections are not subject to the Same-Origin Policy. The browser attaches an
Originheader to the handshake request, but it is entirely up to the server implementation whether to validate it and reject the request. Skipping that check opens the door to Cross-Site WebSocket Hijacking (CSWSH), which abuses an already-authenticated session.
Its role in a P2P context
WebSocket itself is a client-server technology, but it plays an important supporting role around P2P systems.
- Signaling channel for WebRTC: as covered in NAT traversal, WebRTC lets peers exchange media and data directly, but it deliberately does not specify how peers exchange the SDP offer/answer and ICE candidates needed before that connection can be established; that "signaling" mechanism is left open. Most implementations use WebSocket to talk to a signaling server for that pre-negotiation step.
- Relay communication in Nostr: in Nostr, the exchange of
EVENT,REQ, andCLOSEJSON messages between clients and relays runs directly over WebSocket, making it a core part of the protocol itself rather than an implementation detail. - Subscription APIs on blockchain nodes: JSON-RPC interfaces on nodes such as Ethereum expose subscription endpoints like
eth_subscribeover WebSocket, letting a client receive new blocks or logs in real time without resorting to polling. - Real-time state sync for virtual worlds: it is also the default choice for low-latency bidirectional communication in scenarios like P2P and the metaverse, where a server needs to keep many participants' real-time state in sync.
Operational challenges
Unlike short-lived, stateless HTTP requests, WebSocket assumes you will hold open a large number of stateful, long-lived connections at once. That assumption brings its own set of operational headaches.
- Scaling: many general-purpose L7 (application-layer) load balancers are designed around short-lived connections and do not distribute long-lived ones well. The number of connections a single server can hold at once is bounded by OS resources like file descriptors, and a "reconnection storm" (a flood of clients reconnecting simultaneously right after a deploy or a network blip) needs to be managed with exponential backoff and jitter.
- Compatibility with proxies and corporate firewalls: some older proxies and firewalls mishandle the
Upgradeheader, or forcibly close idle connections after a short timeout, and connectivity through corporate networks is a recurring source of trouble. - Its place in the HTTP/2 and HTTP/3 era: HTTP/2 was designed to multiplex many requests over a single connection, but its original specification did not play well with WebSocket's
Upgrademechanism. RFC 8441 (2018) closed that gap, defining a way to tunnel WebSocket inside a single HTTP/2 stream using the extended CONNECT method. HTTP/3, built on QUIC, and WebTransport are the newer options that extend this same line of thinking.
It is worth noting that Socket.IO, often confused with WebSocket, is not the same thing. It is a higher-level library with its own handshake and message framing that automatically falls back to HTTP long polling when WebSocket is unavailable, and it adds features like reconnection and room-based broadcasting. It cannot interoperate directly with a plain WebSocket client.
Comparing polling, SSE, WebSocket, and WebTransport
WebSocket is far from the only way to get real-time data from server to client; simpler mechanisms and newer ones are both frequently the better fit, depending on the use case.
| Aspect | Polling / long polling | SSE | WebSocket | WebTransport |
|---|---|---|---|---|
| Direction | Client-initiated, effectively one-way (server to client) | One-way, server to client | Bidirectional, full-duplex | Bidirectional, with multiple streams plus datagrams |
| Multiplexing | A new request is issued for each round | A single HTTP connection carries a continuous one-way event stream | A single logical stream over one TCP connection | Multiple streams and datagrams multiplexed over QUIC |
| Reliability / ordering | Guaranteed per request | TCP ordering, plus resumption via Last-Event-ID | TCP ordering; reconnection after a drop must be implemented by the application | Choice of reliable, ordered streams or lightweight unordered datagrams |
| Transport | HTTP/TCP | HTTP/TCP | TCP (via an HTTP Upgrade) | QUIC/UDP |
| Browser support maturity | The most battle-tested | Supported in major browsers, though adoption is limited | Mature across all browsers and by far the most widely used | Comparatively new and still maturing; see WebTransport in depth |
Related pages
WebSocket's ideas connect to several other deep dives on this site. WebTransport in depth is the newer alternative that handles streams and datagrams over QUIC; Nostr adopts WebSocket directly as its relay communication channel; NAT traversal is the foundation for finding a signaling peer in the first place; and P2P and the metaverse, where real-time responsiveness matters most, is built on the bidirectional communication WebSocket provides.