πŸ“š Learning Hub
Β· 6 min read

How WebSockets Actually Work Under the Hood


HTTP follows a strict request-response cycle. The client asks, the server answers, and the connection is done. That works beautifully for loading pages and fetching API data, but it falls apart the moment you need the server to push something to the client without being asked first.

Think chat messages, live stock tickers, multiplayer game state, or streaming AI responses. You need a persistent, bidirectional channel β€” and that’s exactly what WebSockets provide.

If you’re new to the concept, start with What is WebSockets?. This post goes deeper: the upgrade handshake, the binary framing protocol, heartbeats, the close sequence, and when you should (and shouldn’t) reach for WebSockets.

The problem with HTTP for real-time

Before WebSockets, developers hacked around HTTP’s limitations with two techniques:

Long polling β€” the client sends a request, and the server holds it open until there’s new data. When the response arrives, the client immediately sends another request. It works, but each cycle carries full HTTP headers, requires a new TCP connection (or at least a new request on a keep-alive connection), and adds latency between each message.

Server-Sent Events (SSE) β€” the server holds a single HTTP response open and streams text events down to the client. SSE is simpler than WebSockets and works over standard HTTP, but it’s unidirectional: the server talks, the client listens. If the client needs to send data back, it has to use a separate HTTP request.

WebSockets solve both problems. One TCP connection, full-duplex communication, minimal framing overhead. Messages flow in both directions at any time.

The upgrade handshake

A WebSocket connection starts life as a regular HTTP request. The client sends a GET with special headers asking to upgrade the protocol:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

The Sec-WebSocket-Key is a random Base64-encoded 16-byte value. The server takes this key, concatenates it with the magic string 258EAFA5-E914-47DA-95CA-5AB0D39D3508 (defined in RFC 6455), computes a SHA-1 hash, and Base64-encodes the result. It sends this back as Sec-WebSocket-Accept:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

The 101 Switching Protocols status code means the server agrees to the upgrade. From this point on, the TCP connection is no longer speaking HTTP β€” it’s a WebSocket connection.

Because the handshake is standard HTTP, it passes through proxies, load balancers, and TLS termination without special handling (mostly β€” more on scaling later). The wss:// scheme runs WebSockets over TLS, just like https:// does for HTTP.

The WebSocket frame format

Once upgraded, data is exchanged in frames. The wire format is compact and binary:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Extended payload length continued, if payload len == 127  |
+-------------------------------+-------------------------------+
|                               | Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------+-------------------------------+
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

Key fields:

  • FIN bit β€” 1 if this is the final fragment of a message. Large messages can be split across multiple frames.
  • Opcode β€” identifies the frame type: 0x1 for text, 0x2 for binary, 0x8 for close, 0x9 for ping, 0xA for pong.
  • MASK bit β€” must be 1 for client-to-server frames. The 4-byte masking key XORs the payload to prevent cache-poisoning attacks on intermediary proxies.
  • Payload length β€” 7 bits for payloads up to 125 bytes, 16 bits if the value is 126, or 64 bits if the value is 127.

The minimum overhead for a small server-to-client message is just 2 bytes. Compare that to the hundreds of bytes of HTTP headers on every long-polling response.

Ping/pong heartbeats

TCP connections can go silent for long periods, and intermediaries (NATs, load balancers, firewalls) may drop idle connections. WebSockets handle this with ping and pong control frames.

Either side can send a ping (opcode 0x9). The other side must respond with a pong (opcode 0xA) containing the same payload. This keeps the connection alive and lets both sides detect if the peer has disappeared.

Most WebSocket libraries handle ping/pong automatically. If you’re building something custom, send a ping every 30 seconds or so and consider the connection dead if no pong arrives within a reasonable timeout.

The close handshake

Closing a WebSocket is a two-step process. One side sends a close frame (opcode 0x8) with an optional status code and reason. The other side responds with its own close frame, and then both sides close the TCP connection.

Common status codes include 1000 (normal closure), 1001 (going away β€” e.g., server shutting down), and 1006 (abnormal closure β€” the connection dropped without a close frame).

This clean shutdown ensures both sides know the connection ended intentionally, not because of a network failure.

WebSocket vs SSE vs long polling

FeatureWebSocketSSELong Polling
DirectionBidirectionalServer β†’ ClientSimulated bidirectional
Protocolws:// / wss://HTTPHTTP
Overhead per message2–14 bytes~50+ bytes (text-based)Full HTTP headers
Browser supportUniversalUniversal (no IE)Universal
Auto-reconnectManualBuilt-inManual
Binary dataYesNo (text only)Yes (with encoding)

Use SSE when you only need server-to-client streaming (notifications, live feeds, AI token streaming). It’s simpler, works with standard HTTP infrastructure, and reconnects automatically.

Use WebSockets when both sides need to send messages freely β€” chat, collaborative editing, multiplayer games, live trading.

Use long polling only as a fallback when neither WebSockets nor SSE are available (increasingly rare).

When to use WebSockets (and when not to)

WebSockets shine for:

  • Chat and messaging β€” low-latency, bidirectional message flow.
  • Live data dashboards β€” stock prices, sports scores, monitoring metrics pushed as they change.
  • Multiplayer gaming β€” game state synchronized across clients in real time.
  • Collaborative editing β€” multiple users typing in the same document.

WebSockets are overkill when:

  • Standard CRUD operations β€” REST with JSON works perfectly. Don’t open a persistent connection to fetch a user profile.
  • Infrequent updates β€” if data changes every few minutes, simple polling or SSE is simpler and cheaper.
  • One-directional streaming β€” SSE is a better fit and doesn’t require protocol upgrades.

Scaling challenges

A single server can hold tens of thousands of WebSocket connections. The real challenge is horizontal scaling.

Sticky sessions β€” because a WebSocket is a persistent connection to a specific server, load balancers must route all traffic for a given connection to the same backend. This means layer-7 load balancers need to support connection upgrades, and you lose the ability to freely redistribute connections.

Cross-server messaging β€” if User A is connected to Server 1 and User B is connected to Server 2, a chat message from A to B needs to hop between servers. The standard solution is a pub/sub broker like Redis. Server 1 publishes the message to a Redis channel, Server 2 subscribes and forwards it to User B. Redis Pub/Sub, Redis Streams, or dedicated message brokers like NATS all work here.

Connection state β€” if a server crashes, all its WebSocket connections die. Clients need reconnection logic with exponential backoff, and your application needs to handle rehydrating state (missed messages, current room membership, etc.) on reconnect.

Resource limits β€” each WebSocket connection holds an open file descriptor and some memory. Monitor your connection counts, set sensible limits, and plan capacity accordingly.

Wrapping up

WebSockets are elegantly simple once you see the pieces: an HTTP upgrade handshake bootstraps a persistent TCP connection, data flows in compact binary frames, ping/pong keeps things alive, and a close handshake tears it down cleanly.

The protocol itself is straightforward. The complexity lives in the infrastructure around it β€” scaling across servers, handling reconnections gracefully, and choosing WebSockets only when the problem actually demands bidirectional, real-time communication. For everything else, HTTP and SSE remain the simpler, better choice.