How WebSockets Work: Handshake, Frames, and Persistent Connections
HTTP can fetch a page. TCP can keep a reliable byte stream open. Neither explains why a live chat box updates without a refresh or why a trading dashboard streams ticks. That job belongs to WebSockets, defined in RFC 6455.
A WebSocket is a persistent, bidirectional channel that starts as an HTTP request and then stops behaving like HTTP. After an upgrade handshake, both sides send framed messages whenever they have something to say.
Why polling is not enough
HTTP is request-response. Short polling repeats empty questions. Long polling rebuilds an HTTP exchange after every event. Server-Sent Events push one-way text. Use WebSockets when both sides send small messages at unpredictable times.
Handshake
The client sends HTTP/1.1 GET with Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key (16 random bytes, Base64), Sec-WebSocket-Version: 13, and Origin. The server replies 101 Switching Protocols with Sec-WebSocket-Accept = Base64(SHA1(key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)). Status 200 is not a socket. TLS for wss:// is finished before this GET. See How HTTP Works, How TCP Works, and How HTTPS Works.
Frames, masking, ping, close
After 101, traffic is frames. Header bits carry FIN, RSV, opcode, MASK, and length. Opcodes: 0 continuation, 1 text, 2 binary, 8 close, 9 ping, 10 pong. Clients must mask payloads with a 4-byte XOR key. Servers must not. Masking is not encryption; it confuses intermediaries that still expect HTTP.
Ping and pong detect dead peers that TCP has not yet noticed. A clean shutdown is a close frame, then a close reply. Code 1000 is normal, 1001 going away, 1002 protocol error. 1006 is local only when TCP dies without a close frame.
Servers, auth, and when not to use them
Browsers hide frames behind WebSocket.send. Auth is usually a ticket in the URL, a first message, or a same-origin cookie on the upgrade. See How Authentication Works and How Cookies and Sessions Work.
A browser example
const socket = new WebSocket("wss://example.com/chat"); socket.addEventListener("open", () => { socket.send(JSON.stringify({ type: "join", room: "lobby" })); }); socket.addEventListener("message", (event) => { render(JSON.parse(event.data)); });
Misconceptions
Masking is not confidentiality. wss:// is. A 200 response is ordinary HTTP, not a WebSocket. Browsers limit concurrent sockets per destination and mobile networks drop idle connections, so reconnect with backoff.
A server must unmask, bound message size, answer pings, and fan out across instances with pub/sub. Idle sockets cost file descriptors. Prefer HTTP for request-reply work and SSE for one-way streams. WebSockets are not UDP; they run on TCP.