How WebSockets Work: The Handshake, Frames and When to Use SSE Instead
Inside the HTTP upgrade and tiny frames that let servers talk first

A chat message lands the instant your friend hits send. A stock ticker twitches without you refreshing. A multiplayer cursor glides across a shared document. Ordinary HTTP was built for "ask, then wait for an answer," so how does the server get to speak first?
Very often the answer is a WebSocket. This guide explains how WebSockets work: the clever HTTP upgrade that opens the connection, the small binary frames that carry every message, how connections stay alive and shut down cleanly, and when a simpler tool like Server-Sent Events is the better choice.
Why the web needed WebSockets
Classic HTTP is request and response. The browser asks, the server answers, and the exchange is over. To fake real-time updates, developers used tricks such as polling (asking "anything new?" every few seconds) and long polling (holding a request open until the server has news). Both work, but each update carries full HTTP headers, and the server still cannot push whenever it likes.
The WebSocket protocol, standardized by the IETF as RFC 6455 in 2011, fixes this with one long-lived, full-duplex connection. Once it is open:
- Either side can send at any time, with no request needed first.
- Messages are lightweight, with a frame header of just a few bytes instead of a full set of HTTP headers.
- Text and binary data are both supported natively.
- It runs over the usual web ports:
ws://defaults to port 80 andwss://, which runs over TLS, defaults to port 443.
How WebSockets work: the opening handshake
A WebSocket begins life as a normal HTTP GET request that asks the server to switch protocols. That design lets it pass through existing web servers, load balancers and firewalls that already understand HTTP.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The Sec-WebSocket-Key is a random value from the client. To prove it really understands WebSockets (and is not an ordinary HTTP server replaying a cached response), the server appends a fixed GUID defined in the RFC, 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, hashes the result with SHA-1 and base64-encodes it. You can check the math yourself:
printf '%s' 'dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11' \
| openssl sha1 -binary | base64
# s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After the 101 Switching Protocols response, HTTP is finished. The same TCP connection now speaks the WebSocket framing protocol. If you use wss://, all of this happens inside an encrypted TLS session, the same one we described in our guide to the HTTPS handshake.
Frames: the tiny envelopes behind every message
Once connected, data travels in frames. Each frame starts with a compact header:
- FIN bit: marks the last fragment of a message, so large messages can be split across several frames.
- Opcode: says what the frame contains.
- Mask bit and key: whether the payload is masked, plus a 4-byte masking key if so.
- Payload length: values up to 125 fit in 7 bits; 126 means a 16-bit length follows, and 127 means a 64-bit length follows.
| Opcode | Frame type | Used for |
|---|---|---|
0x0 | Continuation | Later fragments of a split message |
0x1 | Text | UTF-8 text, such as JSON |
0x2 | Binary | Images, audio, game state, protobuf |
0x8 | Close | Starting or confirming shutdown |
0x9 | Ping | Checking the other side is alive |
0xA | Pong | Answering a ping, or a one-way heartbeat |
Why clients mask their frames
Every frame a client sends must be masked: each payload byte is XORed with a fresh, unpredictable 4-byte key. Servers must not mask, and either side must close the connection if the rule is broken. Masking is not encryption, because the key travels in the frame. Its job, per the RFC, is to stop attacker-controlled bytes from looking like valid HTTP to confused proxies and caches sitting in the middle, which could otherwise be tricked into poisoning their caches.
Keeping connections alive and closing them cleanly
Ping and pong are control frames that can be slipped in between fragments of a larger message. When one side receives a ping, it must reply with a pong carrying the same data. Servers commonly ping idle clients so that dead connections are detected and so that network devices do not quietly drop a connection that looks idle.
Closing is a handshake too. One side sends a close frame with an optional 2-byte status code and reason; the other replies with its own close frame; then the TCP connection is shut. In the browser, the close event exposes that code:
1000Normal closure: the job is done.1001Going away: a server shutting down or a user leaving the page.1006Abnormal closure: a reserved code the browser reports when the connection died without any close frame, often a network drop.1008Policy violation,1009message too big and1011internal server error.4000to4999: private codes your own app can define.
Using WebSockets in the browser
The browser API is small: create a socket, then listen for open, message, error and close.
const socket = new WebSocket("wss://example.com/chat");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "hello" }));
});
socket.addEventListener("message", (event) => {
console.log("Server says:", event.data);
});
socket.addEventListener("close", (event) => {
if (event.code !== 1000) {
// Not a clean close: reconnect with exponential backoff
}
});
Two production details are easy to miss. First, a WebSocket never reconnects by itself, so you need your own retry logic, ideally with backoff and jitter so thousands of clients do not stampede your server at once. Second, send() only queues data. MDN describes bufferedAmount as the count of queued bytes not yet sent to the network, and warns that the classic API has no built-in backpressure. Check bufferedAmount before pushing large volumes, or look at the newer WebSocketStream API, which handles backpressure through streams.
On the server, remember that each client holds an open connection. Scaling out across several servers usually means a shared message bus so a message published on one machine reaches sockets on another, and load balancers configured with idle timeouts longer than your ping interval.
WebSockets vs. Server-Sent Events vs. polling
WebSockets are powerful, but they are not always the simplest fit. If data only flows from server to browser, Server-Sent Events (SSE) often wins: it runs over plain HTTP, reconnects automatically and can resume from the last event ID.
| WebSockets | Server-Sent Events | Polling | |
|---|---|---|---|
| Direction | Both ways | Server to client only | Client asks repeatedly |
| Data | Text and binary | UTF-8 text only | Anything HTTP carries |
| Reconnect | Write it yourself | Built in, with Last-Event-ID | Each poll is fresh |
| HTTP/2 multiplexing | Separate connection per socket by default | Yes, many streams share one connection | Yes |
| Best for | Chat, games, collaboration | Notifications, feeds, AI token streaming | Infrequent updates |
WebSockets can also be carried over newer HTTP versions: RFC 8441 defines an extended CONNECT method for HTTP/2, and RFC 9220 does the same for HTTP/3, though support depends on your browser, server and proxies.
Key takeaways
- A WebSocket starts as an HTTP request and upgrades to a persistent, two-way connection with a 101 response.
- The Sec-WebSocket-Accept value is a SHA-1 hash of the client key plus a fixed GUID, proving the server speaks the protocol.
- Messages travel in small frames; client frames are masked to protect intermediaries, not to encrypt.
- Ping/pong keeps connections healthy, and close codes tell you why a socket ended.
- Always use wss://, check Origin, build your own reconnect logic and watch bufferedAmount.
- For one-way server updates, Server-Sent Events is often simpler.
Frequently asked questions
Are WebSockets secure?
They are as secure as you make them. Use wss:// so traffic is encrypted with TLS, verify the Origin header, authenticate users during the handshake and validate every incoming message just as you would any other input.
Do WebSockets replace HTTP APIs?
No. They complement them. Regular HTTP remains better for one-off requests, caching and simple CRUD operations, while WebSockets suit continuous two-way streams of small messages.
Why does my WebSocket keep closing with code 1006?
Code 1006 means the connection ended without a close frame, usually because of a network drop, a crashed server or a proxy or load balancer timing out an idle connection. Regular pings and longer idle timeouts often help.
Should I use Socket.IO or raw WebSockets?
Libraries like Socket.IO add features such as automatic reconnection, rooms and fallbacks, but they use their own protocol on top, so a plain WebSocket client cannot talk to them directly. Raw WebSockets keep things lighter and standard.
Sources
New to how browsers find servers in the first place? Start with how DNS turns a web address into an IP.











Comments
No comments yet — be the first to share your thoughts.
Join the discussion