How WebSockets work
How WebSockets give a web page a lasting two-way connection to a server, from the HTTP upgrade that opens it to frames, masking, pings and the closing handshake, and how they run over HTTP/2 and HTTP/3.
The web was built on a simple pattern: the browser asks, the server answers. That works well for pages, but not for a chat, a multiplayer game, a shared document or a live dashboard, where the server has news to deliver at moments the browser cannot predict.
WebSocket solves this. After a short handshake, a web page and a server share one lasting
connection, and either side can send a message at any time. The protocol is defined in RFC
6455 (2011), and the WHATWG WebSockets Standard defines the WebSocket object that web pages use.
This article follows a connection from the first request to the last frame.
Before WebSockets: asking again and again
Plain HTTP gives a server no way to speak first. RFC 6202 describes the workarounds that sites used instead:
- Polling. The page asks for news at regular intervals. When nothing has happened, the answer is empty. The shorter the interval, the faster news arrives, but the more requests the server and the network have to handle.
- Long polling. The page sends a request and the server holds it open, answering only when there is news or a timeout passes. The page then immediately sends the next request, so there is almost always one waiting.
Long polling delivers news quickly, but it is still a workaround. RFC 6455 lists the costs of such techniques: the server needs several connections per client, every message from the client carries a full set of HTTP headers, and the page has to match replies to requests itself. WebSocket replaces all of this with a single connection that carries traffic in both directions.
The opening handshake
A WebSocket connection starts as an ordinary HTTP request over a
TCP connection, so it can use the same ports as the web: 80 for
ws:// URLs and 443 for wss:// URLs, which are protected by TLS.
The browser asks the server to switch protocols, using the example from RFC 6455:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
- Upgrade and Connection ask to turn this connection into a WebSocket connection.
- Sec-WebSocket-Key is a random 16-byte value, encoded in base64 and chosen fresh for every connection. (The key in this example decodes to the text “the sample nonce”.)
- Origin tells the server which site the page that opened the connection comes from.
- Sec-WebSocket-Protocol lists application protocols, called subprotocols, that the page can speak over the connection.
- Sec-WebSocket-Version is always 13, the version RFC 6455 defines.
A server that agrees answers with status 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
The server has picked the chat subprotocol from the list. The Sec-WebSocket-Accept value
proves that it really understood the WebSocket request: it appends a fixed identifier defined in
RFC 6455, 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, to the key, hashes the result with SHA-1 and
encodes the hash in base64.
key: dGhlIHNhbXBsZSBub25jZQ==
key + id: dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
accept: base64(SHA-1(key + id)) = s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
This is not a security check of the server’s identity; that is the job of TLS. It makes sure that
both sides really speak WebSocket. An ordinary web server cannot produce the right answer by
accident, and a web page cannot fake a WebSocket request with a form or a script request, because
the Fetch Standard forbids pages to set header fields whose names start with Sec-. If the status
is not 101 or the accept value is wrong, the browser does not open the connection.
Frames and messages
After the 101 response, HTTP is over. Both sides now send frames, a compact binary format. Every frame starts with a two-byte header:
- FIN marks the last frame of a message.
- RSV1–RSV3 are reserved for extensions and are 0 otherwise.
- The opcode says what the frame carries.
- MASK says whether the payload is masked, which is the case for every frame from the client (see the next section).
- The payload length is stored in 7 bits when it is 125 bytes or less. The value 126 means that the real length follows in the next 2 bytes, and 127 means it follows in the next 8 bytes.
RFC 6455 defines six frame types:
| Opcode | Frame | Purpose |
|---|---|---|
| 0x1 | Text | Text, which must be valid UTF-8 |
| 0x2 | Binary | Any bytes; their meaning is up to the app |
| 0x0 | Continuation | The next part of a message split into frames |
| 0x8 | Close | Starts or answers the closing handshake |
| 0x9 | Ping | Checks that the other side is still there |
| 0xA | Pong | Answers a ping |
The smallest frame shows how compact this is. Sent from the server, the text message “Hello” is seven bytes:
81 05 48 65 6c 6c 6f
0x81 is binary 1000 0001: FIN is set and the opcode is 1, a text frame. 0x05 means not masked
and a length of 5. The remaining five bytes are “Hello” in UTF-8.
A long message can be split into several frames, for example when the sender does not know its
total size in advance. The first frame carries the real opcode, the following ones use opcode 0,
and the last one has FIN set. “Hello” sent as “Hel” and “lo” becomes 01 03 48 65 6c and
80 02 6c 6f. Control frames such as pings may be sent between the parts of a split message, but
they can never be split themselves and carry at most 125 bytes.
Masking: protecting the network in between
Every frame a client sends is masked: the client picks a new random 4-byte masking key
for each frame, writes it into the frame, and combines each payload byte with one byte of the key
using XOR. The receiver applies the same XOR with the same key to recover the data. Here is the
masked “Hello” from RFC 6455, with the key 37 fa 21 3d:
Masking is not encryption: the key travels in the same frame, so anyone who can read the frame can unmask it. Its purpose is different. While WebSocket was being designed, researchers showed that a script could send bytes through an upgraded connection that looked like an HTTP request, and some proxies and caches on the way would believe it and store a forged response for other users. With an unpredictable key on every frame, a script can no longer choose which bytes appear on the wire, so it cannot build such a fake request.
Only frames from the client to the server are masked, because the attack needs a forged request. A server must close the connection if a client frame is not masked, and a client must close it if a server frame is masked. Masking applies even over TLS.
Ping, pong and closing
Ping and pong keep a connection healthy. Either side can send a ping at any time, and the other must answer with a pong carrying the same data. A ping can serve as a keepalive or check that the other side is still responsive. A pong can also be sent on its own as a one-way heartbeat.
Closing is a handshake of its own. The side that wants to stop sends a Close frame, usually with a two-byte status code and an optional reason. The other side answers with its own Close frame, and then the TCP connection is closed, with the server closing it first. Waiting for the answer avoids cases where data that was already sent gets lost, because both sides know that no more data is coming. Some of the status codes defined in RFC 6455:
| Code | Meaning |
|---|---|
| 1000 | Normal closure: the purpose of the connection is fulfilled |
| 1001 | Going away, for example a server shutting down or a page left |
| 1002 | Protocol error |
| 1003 | A type of data the endpoint cannot accept |
| 1007 | Data that does not match its type, such as invalid UTF-8 |
| 1008 | A message that violates the endpoint’s policy |
| 1009 | A message too big to process |
| 1011 | An unexpected condition on the server |
Three codes are never sent in a frame. Programs use them to report what happened: 1005 when a Close frame had no code, 1006 when the connection ended without any Close frame, and 1015 when the TLS handshake failed.
Subprotocols and extensions
WebSocket only moves messages; it does not say what they mean. Applications agree on that through
a subprotocol, chosen in the handshake with Sec-WebSocket-Protocol, as in the chat example
above.
Extensions change the frames themselves. RFC 7692 defines permessage-deflate, which
compresses each message with the DEFLATE algorithm. The client offers it in a
Sec-WebSocket-Extensions header; if the server accepts, compressed messages are marked with the
RSV1 bit of their first frame.
WebSockets over HTTP/2 and HTTP/3
The handshake above relies on HTTP/1.1 features that HTTP/2 and HTTP/3 do not have: they carry many requests side by side on one connection, so a single request cannot switch the whole connection to another protocol. Over HTTP/1.1, every WebSocket therefore needs its own TCP connection.
RFC 8441 solves this for HTTP/2 with an extended CONNECT request. The server first announces
support with the setting SETTINGS_ENABLE_CONNECT_PROTOCOL. The client then opens a new stream with
the method CONNECT and the pseudo-header :protocol set to websocket, and a successful
response, such as status 200 in the example of RFC 8441, turns that one stream into a WebSocket
connection. The key and accept values are not needed. The frames are exactly the same, and the
other streams on the connection carry ordinary requests or further WebSockets. RFC 9220 does the
same for HTTP/3, which runs over QUIC.
WebSockets in the browser
Web pages use the WebSocket object from the WHATWG WebSockets Standard:
const socket = new WebSocket("wss://chat.example.com/room", ["chat"]);
socket.addEventListener("open", () => {
socket.send("Hello");
});
socket.addEventListener("message", (event) => {
console.log("received", event.data);
});
socket.addEventListener("close", (event) => {
console.log("closed", event.code, event.reason, event.wasClean);
});
- The constructor accepts
ws:andwss:URLs, and alsohttp:andhttps:URLs, which it treats asws:andwss:. The optional second argument lists the subprotocols the page can speak. readyStatereports the connection state:CONNECTING(0),OPEN(1),CLOSING(2) orCLOSED(3).send()accepts text, which becomes a text message, or binary data such as anArrayBufferor aBlob. Received binary messages arrive as aBlobby default, or as anArrayBufferafter settingbinaryTypeto"arraybuffer".bufferedAmounttells how many bytes were passed tosend()but not yet transmitted, so a page can slow down when the network cannot keep up.close(code, reason)starts the closing handshake; thecloseevent reports the code and whether the connection closed cleanly.
The browser handles the handshake, framing, masking and pings itself; a page only ever sees whole messages.
Security
- Use
wss://. Masking does not protect the data; TLS is what keeps it private and proves the server’s identity. - Check the origin. Pages from other sites can also try to open a WebSocket to a server, and
the browser sends the page’s origin with the request. RFC 6455 says that a server meant only for
certain sites should check the
Originheader and answer other origins with status 403 Forbidden. - Authenticate the handshake. The handshake is an HTTP request, so it can carry cookies or other credentials, and the server should decide there whether to accept the connection.
- Treat every message as untrusted input, and limit message sizes; a server can close a connection that sends messages too big to process with code 1009.
When to use WebSockets
WebSockets fit when both sides need to send often and with little delay: chats, games, collaborative editing, stock tickers. They are not always needed:
- When only the server has to send updates, server-sent events from the HTML Standard may
be simpler. The page opens an
EventSource, and the server keeps an ordinary HTTP response open and writes events into it in thetext/event-streamformat; the browser reconnects automatically if the connection drops. - For ordinary request and response, plain HTTP remains the right tool, with its caching, status codes and tooling.
Key points
- A WebSocket is a lasting, two-way connection between a page and a server, defined in RFC 6455; either side can send a message at any time.
- It starts with an HTTP/1.1 Upgrade request. The server answers 101 and proves it understood
by hashing the client’s key into
Sec-WebSocket-Accept. - Data travels in frames with a two-byte header; messages can be text or binary and can be split across several frames. Ping, pong and close are control frames.
- Frames from the client are masked with a random key, to protect proxies on the way, not to
hide the data; use
wss://for privacy. - Over HTTP/2 and HTTP/3, each WebSocket uses one stream of a shared connection, opened with extended CONNECT (RFC 8441 and RFC 9220).
Sources
- The WebSocket Protocol (RFC 6455)I. Fette, A. Melnikov, RFC Editor, 2011Accessed
- WebSockets StandardWHATWGAccessed
- Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP (RFC 6202)S. Loreto, P. Saint-Andre, S. Salsano, G. Wilkins, RFC Editor, 2011Accessed
- Compression Extensions for WebSocket (RFC 7692)T. Yoshino, RFC Editor, 2015Accessed
- Bootstrapping WebSockets with HTTP/2 (RFC 8441)P. McManus, RFC Editor, 2018Accessed
- Bootstrapping WebSockets with HTTP/3 (RFC 9220)R. Hamilton, RFC Editor, 2022Accessed
- Fetch StandardWHATWGAccessed
- HTML Standard: Server-sent eventsWHATWGAccessed