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.

Published 13 min readThe webNetworking

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.

Long pollingBrowserServerrequestnewsrequestnewsrequestnewsWebSocketBrowserServerupgrade101messagesserver holds the requestone small frameIn long polling, every arrow is a full HTTP message.
Long polling needs a new request after every answer. A WebSocket stays open, and both sides send whenever they have something to say.

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.

BrowserServerTCP and TLS handshakesGET /chatUpgrade: websocketSec-WebSocket-Key101 Switching ProtocolsSec-WebSocket-AcceptText: HelloText: Hi therePingPongClose 1000Close 1000TCP connection closed
A whole WebSocket connection: one HTTP request and response, then frames in both directions until the closing handshake.

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:

01234567FINRSV1–3extensionsopcodeframe typeMASKpayload length0–125, or 126 / 127extended payload length0, 2 or 8 bytesmasking key0 or 4 bytes · client frames onlypayload datathe message, or a part of itBits of the first two bytes, numbered from 0.
The WebSocket frame: two bytes of header, an optional longer length, a masking key in frames from the client, then the data.
  • 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:

Hellodata48656c6c6fXORXORXORXORXORkey37fa213d37=====masked7f9f4d5158The fifth byte reuses the first byte of the key.
Masking XORs each byte with one byte of a fresh random key. The same operation removes it again.

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.

HTTP/1.1WebSocket: chatTCP connection 1WebSocket: pricesTCP connection 2Page requestsTCP connection 3HTTP/2One TCP connectionstream 1GET /stream 3WebSocket: chatstream 5GET /app.jsstream 7WebSocket: prices
Over HTTP/1.1 each WebSocket takes a whole connection. Over HTTP/2 and HTTP/3 it takes one stream of a shared connection.

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: and wss: URLs, and also http: and https: URLs, which it treats as ws: and wss:. The optional second argument lists the subprotocols the page can speak.
  • readyState reports the connection state: CONNECTING (0), OPEN (1), CLOSING (2) or CLOSED (3).
  • send() accepts text, which becomes a text message, or binary data such as an ArrayBuffer or a Blob. Received binary messages arrive as a Blob by default, or as an ArrayBuffer after setting binaryType to "arraybuffer".
  • bufferedAmount tells how many bytes were passed to send() but not yet transmitted, so a page can slow down when the network cannot keep up.
  • close(code, reason) starts the closing handshake; the close event 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 Origin header 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 the text/event-stream format; 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

  1. The WebSocket Protocol (RFC 6455)I. Fette, A. Melnikov, RFC Editor, 2011Accessed
  2. WebSockets StandardWHATWGAccessed
  3. 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
  4. Compression Extensions for WebSocket (RFC 7692)T. Yoshino, RFC Editor, 2015Accessed
  5. Bootstrapping WebSockets with HTTP/2 (RFC 8441)P. McManus, RFC Editor, 2018Accessed
  6. Bootstrapping WebSockets with HTTP/3 (RFC 9220)R. Hamilton, RFC Editor, 2022Accessed
  7. Fetch StandardWHATWGAccessed
  8. HTML Standard: Server-sent eventsWHATWGAccessed