WebSocket Viewer
How do chat apps get new messages instantly?
Loading the simulation…
How do chat apps get new messages instantly?
Loading the simulation…
An ordinary HTTP/1.1 GET asks to stop being HTTP. The server derives Sec-WebSocket-Accept step by step, answers 101, and from the blank line onward the same TCP connection carries frames in both directions at eleven bytes a message.
5 steps. Pause, step back or replay any of them.
Nothing selected
Click anything on the map (a device, a cable, or a moving message) to see what it is.
GET /chat?room=lobby HTTP/1.1
Host: chat.example.com
highlighted: Upgrade: websocket
highlighted: Connection: Upgrade
highlighted: Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
highlighted: Sec-WebSocket-Version: 13
Origin: https://app.example.com
highlighted: Sec-WebSocket-Protocol: chat.v2, chat.v1
HTTP/1.1 101 Switching Protocols
highlighted: Upgrade: websocket
highlighted: Connection: Upgrade
highlighted: Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
highlighted: Sec-WebSocket-Protocol: chat.v1
keep-alive, Upgrade, and a server comparing the whole value to "Upgrade" rejects them.permessage-deflate claims RSV1. A server may never grant one that was not offered.A proof of comprehension, not a secret. Nothing authenticates on this digest, which is why SHA-1 is not a mistake here — a collision buys an attacker nothing. What it proves is that the responder read the request and knew what was being asked for, so a cached 101 from an earlier connection cannot answer this one.
dGhlIHNhbXBsZSBub25jZQ==The client’s Sec-WebSocket-Key: 16 random bytes in base64, so always 24 characters ending in "==". It is used verbatim -- not decoded, not trimmed, not re-encoded. A server that normalises it computes a different digest and every client rejects the answer.
258EAFA5-E914-47DA-95CA-C5AB0DC85B11A constant, identical in every implementation, chosen because it is unlikely to appear in an endpoint that does not speak WebSocket. It is public: it is printed in the RFC. Concatenation only -- no separator, no whitespace, no newline.
dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11Key and GUID run together. The "==" padding on the key sits in the middle of the string and is hashed along with everything else.
b3 7a 4f 2c c0 62 4f 16 90 f6 46 06 cf 38 59 45 b2 be c4 eaTwenty bytes, always. SHA-1 is specified here despite being broken for signatures, and that is not an oversight: no signature rests on this digest, so a collision buys an attacker nothing. It proves comprehension, not identity.
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Standard base64 with padding -- not the URL-safe alphabet -- because this is an HTTP field value, not a URL component. Twenty bytes always encode to 28 characters ending in a single "=". This exact string goes in Sec-WebSocket-Accept, and the client compares it byte for byte against its own copy of this computation.
| Requirement | Check | Found |
|---|---|---|
| MUSTpassed | Method is GET A GET is the request every proxy, load balancer, and firewall on the path is certain it understands. The handshake has no body and creates nothing, so no other method would earn its risk. RFC 6455 § 4.1 | GET |
| MUSTpassed | HTTP version is at least 1.1 Upgrade and Connection are hop-by-hop fields that need a persistent connection to switch. HTTP/1.0 has no dependable notion of one. HTTP/2 has no connection-wide upgrade at all -- RFC 8441 replaces this handshake with an extended CONNECT. RFC 6455 § 4.1 | HTTP/1.1 |
| MUSTpassed | Host is present Required of every HTTP/1.1 request, and load-bearing here: one IP address serves many WebSocket endpoints, and Host is what picks between them before any WebSocket-specific field is even read. RFC 6455 § 4.1 | chat.example.com |
| MUSTpassed | Upgrade field contains the "websocket" token Compared as a case-insensitive token, so "WebSocket" and "websocket" are the same request. Upgrade is a list field and predates WebSockets by a decade -- it was HTTP/1.1’s general answer to "switch this connection to another protocol". RFC 9110 § 7.8 | websocket |
| MUSTpassed | Connection field lists the "Upgrade" token The token must be present in the list, not equal to the whole value. Browsers send "Connection: keep-alive, Upgrade", and a server that string-compares the field against "Upgrade" rejects them and then blames the browser. This is the single most common handshake bug there is. RFC 9110 § 7.8 | Upgrade |
| MUSTpassed | Sec-WebSocket-Key is base64 of exactly 16 bytes A fresh nonce per connection. The server hashes it with a fixed public GUID to produce Sec-WebSocket-Accept, which proves the server understood the handshake rather than being an HTTP server that answered 101 by coincidence. RFC 6455 § 4.1 | dGhlIHNhbXBsZSBub25jZQ== |
| MUSTpassed | Sec-WebSocket-Version is one the server supports (13) Version 13 is the RFC; 0 through 12 were drafts that shipped in browsers while the working group iterated. A server that cannot speak the requested version answers 426 with its own Sec-WebSocket-Version list rather than a bare 400, so the client can retry with something it understands. RFC 6455 § 11.3 | 13 |
| SHOULDpassed | Origin is one the server accepts Accepted: https://app.example.com. The browser sets Origin and page script cannot forge it, so this check works -- against browsers. A non-browser client sends whatever it likes, which is why Origin is authorisation about pages, never about users. RFC 6455 § 10.2 | https://app.example.com |
Both directions, ordered by the moment each frame left its sender — which is the only ordering the protocol has. After the handshake there is no request/response pairing at all: two independent streams sharing one TCP connection, and nothing correlates a frame in one with a frame in the other.
Six bytes of text in an eleven-byte frame: two bytes of header, four of masking key, five of payload. The same five characters as an HTTP POST would have cost several hundred bytes of field lines, and would have needed a reply.
The MASK bit is 1 and there is a masking key, because this frame is going client to server. Every one of them is, without exception.
Each row is 32 bits, laid out as RFC 6455 § 5.2 draws it. The byte offset is in the gutter. The header is 6 bytes on this frame — never a fixed size, and never an odd number: the length extension is 0, 2, or 8 bytes and the mask is 0 or 4, so the only reachable header sizes are 2, 4, 6, 8, 10, and 14.
81 85 F0 17 03 F4 98 72 6F 98 9FPayload as text: hello
6 bytes of header for 5 of payload — 55% overhead. The equivalent HTTP request would have cost several hundred bytes of field lines and needed a reply.
Not for privacy -- the key is in the frame, four bytes ahead of the data it masks. It is there so that page script cannot choose the bytes that appear on the wire. Without it, a script could write a payload that spells out an HTTP request, and a transparent proxy that never understood the upgrade could be induced to parse it as one and poison its cache for every other user. A fresh unpredictable key per frame makes the wire bytes effectively random, so nothing can be planted in them.
transformed[i] = original[i] XOR key[i mod 4] — its own inverse, which is why one routine serves both ends. It is not encryption: the key is in the clear, four bytes ahead of the data it masks.
And the encoding must be the minimal one. A 100-byte payload sent with the 16-bit escape is a protocol error, not merely wasteful — two legal spellings of one frame is how two parsers come to disagree about where the next frame starts.
7-bitthis frame0 to 125 bytes
7-bit field holds the length itself. Header: 2 (unmasked) or 6 (masked).
The common case, and the reason a chat message costs almost nothing to send. Seven bits could have held 127, but 126 and 127 were spent as escapes -- a good trade, since it bought a 64-bit length for two reserved values.
7+16126 to 65,535 bytes
7-bit field holds 126, as an escape. Header: 4 (unmasked) or 8 (masked).
Two more bytes, big-endian, unsigned. Every multi-byte integer in the WebSocket header is big-endian, like every other integer on the Internet: RFC 1700 called it network byte order and the frame header does not get to be different.
7+6465,536 bytes and above
7-bit field holds 127, as an escape. Header: 10 (unmasked) or 14 (masked).
Eight more bytes, big-endian, and the most significant bit MUST be 0 -- so the real ceiling is 2^63-1, not 2^64-1. Reserving the sign bit means a language whose integers are signed cannot be handed a length it will read as negative, which is a whole class of buffer bug refused at the specification level.
| Bit | Field | Value |
|---|---|---|
| 0 +1 | FIN The last frame of this message. A 0 here means the next data frame from this endpoint will be a continuation carrying more of the same message. RFC 6455 § 5.4 | 1 (final frame)1 |
| 1 +1 | RSV1 Reserved. Must be 0 unless an extension negotiated in the handshake claims it -- permessage-deflate (RFC 7692) uses RSV1 to mark a compressed message. A set bit nobody agreed to is a protocol error. RFC 6455 § 5.2 | 00 |
| 2 +1 | RSV2 Reserved. Must be 0 unless an extension negotiated in the handshake claims it. RFC 6455 § 5.2 | 00 |
| 3 +1 | RSV3 Reserved. Must be 0 unless an extension negotiated in the handshake claims it. RFC 6455 § 5.2 | 00 |
| 4 +4 | opcode A message whose payload is UTF-8. Ill-formed UTF-8 must fail the connection with 1007; there is no lenient mode. RFC 6455 § 5.6 | 0x1 (text)0001 |
| 8 +1 | MASK Set on every client-to-server frame, without exception. A server that receives a frame without it must fail the connection. RFC 6455 § 5.3 | 1 (payload is masked)1 |
| 9 +7 | Payload len The length fits in seven bits, so this is the length. 126 and 127 are escapes, which is why the ceiling here is 125 and not 127. RFC 6455 § 5.2 | 50000101 |
| 16 +32 | Masking-key Four bytes, fresh and unpredictable for every frame. payload[i] XOR key[i mod 4], which is its own inverse -- the receiver runs the identical operation to get the data back. It is not encryption: the key is right here, in the clear, immediately before the bytes it masks. RFC 6455 § 5.3 | F0 17 03 F4 |
| 48 +40 | Payload Data UTF-8, and strictly so: an ill-formed sequence must fail the connection with close code 1007. RFC 6455 § 5.6 | 5 bytes |