Start as an HTTP request, then hijack the TCP connection for a persistent, bidirectional, framed message channel.
WebSockets give both sides a symmetric, always-open pipe over a single TCP connection. Either peer sends a message at any time, with almost no per-message overhead.
A WebSocket connection begins as HTTP. The client sends a normal GET with an
Upgrade header; the server answers 101 Switching Protocols and the same TCP
connection stops speaking HTTP and starts speaking the WebSocket frame protocol.
# The client's opening request (over TCP, before upgrade)
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
# The server's response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept handshake: the server concatenates the client's key
with a fixed GUID, SHA-1 hashes it, and base64s the result. It proves the server actually understood the
WebSocket protocol (not a confused HTTP cache), and stops cross-protocol attacks.
After upgrade, data moves in frames. A logical message is one or more frames. Each frame header carries an opcode, a mask bit, and a length.
| Opcode | Meaning |
|---|---|
0x0 | Continuation (part of a fragmented message) |
0x1 | Text frame (UTF-8) |
0x2 | Binary frame |
0x8 | Close |
0x9 / 0xA | Ping / Pong (keepalive) |
Idle connections get silently dropped by NATs, load balancers, and proxies. The ping/pong control frames keep the path warm and let each side detect a dead peer.
0x9 ping; a
conforming peer must reply with a 0xA pong echoing the payload. No pong within your timeout ⇒
treat the connection as dead and reconnect. Most server libraries ping automatically on an interval.
const ws = new WebSocket("wss://example.com/chat", ["chat.v2"]); // subprotocol
ws.onopen = () => ws.send(JSON.stringify({ type: "join", room: "general" }));
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data); // one event == one message (not one frame)
render(msg);
};
ws.onclose = (ev) => {
// 1000 = normal, 1006 = abnormal (no close frame — often a dropped connection)
if (ev.code !== 1000) scheduleReconnect();
};
ws.onerror = () => ws.close();
WebSocket arg) let client and server agree on a
message grammar during the handshake via Sec-WebSocket-Protocol — e.g. graphql-ws,
mqtt, or your own chat.v2. The server echoes the one it accepts.
| Use WebSockets when… | Prefer plain HTTP / SSE when… |
|---|---|
| Both sides send frequently (chat, collab editing, games) | Only the server pushes (feeds, notifications) → SSE |
| You need low per-message overhead on a hot channel | Requests are occasional → normal request/response |
| Sub-second bidirectional latency matters | You want HTTP caching, proxies, and auth "for free" |