← Computer Networks
DNS & the Web Application Layer

WebSockets

Watch: "WebSockets Crash Course - Handshake, Use-cases, Pros & Cons and more" — Hussein Nasser

WebSockets give you a full-duplex, persistent channel over a single TCP connection — the standard answer whenever an interviewer asks how you'd build real-time chat, live dashboards, or multiplayer features without hammering the server with requests. The core shift is structural: HTTP is strictly request→response, one side always initiates; a WebSocket connection, once established, lets either side send a message at any time with no pairing required — genuinely bidirectional, not "very fast polling."

What Is a WebSocket?

A WebSocket is a way for a website or app to keep an open line to a server so either side can send something the moment it happens, instead of the browser having to keep asking "anything new yet?" It starts out as a normal web request, then both sides agree to switch that same connection into a mode where messages can flow in either direction at any time, without one side having to ask first.

Where This Is Used

A Day-to-Day Example

Think of the difference between calling a friend on the phone versus texting them "are you free?" every thirty seconds until they reply. Texting repeatedly (polling) wastes effort and still has gaps between checks. A phone call (a WebSocket) stays open the whole time — either of you can speak the moment you have something to say, with nobody waiting for their turn to ask.

Deep Dive: The Upgrade Handshake

A WebSocket connection is born as an ordinary HTTP request — that's deliberate, so it can traverse the same infrastructure (proxies, load balancers, port 443) as normal HTTP traffic.

WebSocket upgrade then two-way framesClientchat.example.comGET /chat, Upgrade: websocketan ordinary HTTP request101 Switching Protocolssame TCP connection, now WebSocket framesmessage framemessage frame (pushed any time)
A WebSocket starts life as an HTTP request asking to upgrade. After the 101 response the same TCP connection carries small frames in both directions, so the server can push without being asked.
clientserver
GET /chat HTTP/1.1
Host: chat.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

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

Sec-WebSocket-Accept is computed by the server by concatenating the client's Sec-WebSocket-Key with a fixed magic GUID (258EAFA5-E914-47DA-95CA-C5AB0DC85B11), SHA-1 hashing it, and base64-encoding the result — this isn't for security, it's to prove the server actually understood the WebSocket upgrade request and isn't a caching proxy blindly reflecting headers back. Once the client verifies that value matches, the underlying TCP connection stops speaking HTTP entirely and switches to WebSocket framing — no new connection is opened.

Deep Dive: Framing, Heartbeats, and Reconnection

After the upgrade, data flows as a sequence of frames, each tagged with an opcode: 0x1 text, 0x2 binary, 0x8 close, 0x9 ping, 0xA pong. Ping/pong exists for two practical reasons: keeping intermediate proxies/NATs from silently timing out and dropping an idle-looking connection, and letting each side detect a genuinely dead peer (no pong within a timeout window means the connection is presumed dead and should be torn down and re-established, rather than left as a silent zombie holding server resources). A production client needs explicit reconnection logic with backoff — the browser's native WebSocket object does not auto-reconnect on drop, and a naive tight reconnect loop against a server that's actually down turns a blip into a thundering-herd problem.

// Example: trading app streaming price updates, no polling
const ws = new WebSocket("wss://feed.example.com/prices");
ws.onmessage = (event) => {
  const { symbol, price } = JSON.parse(event.data);
  updateTicker(symbol, price);   // pushed the instant price changes, not on an interval
};
ws.onclose = () => scheduleReconnect(); // with backoff + jitter

Deep Dive: Fan-out Across Server Instances

This is the scaling problem interviewers push on hardest. A WebSocket connection is stateful and pins a specific client to a specific server process — if Client A is connected to Server 1 and Client B is connected to Server 2, and an event that Client B cares about is generated on Server 1, Server 1 has no direct way to reach Client B's socket, which physically lives in Server 2's memory.

Fan-out through a shared busclient Aclient Bserver 1server 2Redis Pub/Sub or Kafkashared message buswswspublishsubscribepush
Client A and client B are connected to different servers. A message from A is published to a shared bus; every server subscribes, so server 2 receives it and pushes it down B's open socket.
publisheddelivered
Show text version
Client A ---ws--- Server 1 ---publish---> Redis Pub/Sub (or Kafka)
                                                |
Client B ---ws--- Server 2 <---subscribe-------+
   (Server 2 receives the event and pushes it down Client B's live socket)

The standard fix is a cross-instance backplane: every WebSocket server subscribes to a shared Redis Pub/Sub channel (or Kafka topic); whichever server produces an event publishes it once; every server that has a relevant connected client picks it up from the subscription and forwards it down that specific socket. This decouples "which server generated the event" from "which server holds the recipient's connection."

Key Pointers

How It Works

  1. The client sends an HTTP GET with Upgrade: websocket, Connection: Upgrade, and a Sec-WebSocket-Key header.
  2. The server validates the request and replies 101 Switching Protocols with a Sec-WebSocket-Accept header derived from that key.
  3. The existing TCP connection is now reused for WebSocket framing instead of HTTP — no new connection is opened.
  4. Either side sends text or binary frames at any time; periodic ping/pong frames keep the connection alive through intermediate proxies and NATs.
  5. Either side sends a close frame to terminate the connection gracefully.

Try It Yourself

# Connect to a public echo server from the terminal (npm install -g wscat)
wscat -c wss://echo.websocket.org

# Send a message once connected, then watch it echo back
> hello world

# In Chrome DevTools: Network tab → filter by "WS" → click the connection →
# Messages tab shows every frame sent/received in real time, plus opcode

# Inspect the raw upgrade handshake with curl (won't maintain the connection,
# but shows the 101 response and headers)
curl -v \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://echo.websocket.org

Interview Q&A

Q: Why does a WebSocket handshake start as an HTTP request instead of a totally separate protocol?

A: So it can traverse existing web infrastructure without special-casing — the same port (443), the same TLS termination, the same proxies and load balancers that already understand HTTP can see and route the initial upgrade request. If it were a bespoke protocol from byte one, every piece of middleware between client and server would need to explicitly support it.

Q: How would you scale a chat feature to multiple server instances?

A: Each server accepts and holds WebSocket connections for whichever clients connected to it, but none of them can directly reach a socket living on another instance. I'd put a shared pub/sub layer (Redis Pub/Sub for lower latency and simplicity, or Kafka if I also need durability/replay) between the servers — when any server needs to deliver a message to a client, it publishes the event to the relevant channel/topic, and whichever server actually holds that client's connection picks it up from its subscription and writes it to the socket.

Q: What's the difference between WebSockets and Server-Sent Events, and when would you pick SSE?

A: SSE is server-to-client only, riding plain HTTP (and multiplexed cleanly over HTTP/2), with the browser's native EventSource handling automatic reconnection for you. WebSockets are bidirectional and need their own connection lifecycle management. If the use case is genuinely one-directional — a live activity feed, streaming build logs, price tickers where the client never needs to send anything back on the same channel — SSE is simpler to build and scale, since it doesn't need sticky sessions or a stateful-connection story any more than a normal long-lived HTTP request does.

Q: How do you detect a dead WebSocket connection?

A: TCP alone won't tell you promptly — a connection can look "open" at the OS level long after the peer has actually vanished (phone went into a tunnel, laptop was closed). The application-level fix is ping/pong: the server sends a ping frame on an interval, and if it doesn't get a pong back within a timeout, it treats the connection as dead, closes it, and frees the resources — which also signals the client-side reconnection logic to kick in.

Q: What's backpressure in this context and why does it matter?

A: It's what happens when the server produces messages faster than a given client can consume them — say, a market-data feed during a volatility spike hitting a client on a throttled mobile connection. Without handling it, the server's internal send buffer for that one socket grows without bound, eventually consuming meaningful memory across thousands of connections. Production systems watch the buffered-amount per socket and apply a policy — drop intermediate updates and only send the latest, throttle the send rate, or disconnect the client — rather than buffering unboundedly.

System Design Tie-in

WebSockets are the default answer to "design a chat app," "design a live collaborative document," "design a multiplayer game's state sync," or "design a stock ticker" — and the interviewer is really testing whether you volunteer the fan-out problem unprompted. Expect to draw the Redis Pub/Sub (or Kafka) backplane diagram yourself. Load balancer configuration matters too: you generally need either sticky sessions (so a reconnecting client lands back on roughly the same instance, useful for local state) or a fully stateless design where any instance can serve any client purely via the shared backplane — the latter is more resilient to instance failure. Connection count itself becomes a capacity-planning input that a stateless HTTP API doesn't have — you're now bounded by file descriptors and memory per open connection, not just request throughput.

Trade-offs

Reach for WebSockets when you need genuinely low-latency, bidirectional push — chat, collaborative editing, multiplayer state sync. Reach for SSE when only server-to-client push is needed, since it rides plain HTTP/2 multiplexing and needs no special infrastructure. Fall back to polling only when persistent connections aren't an option (some legacy proxies/load balancers). The most common design mistake is treating a WebSocket like a stateless HTTP request behind a normal load-balanced fleet — without sticky sessions or a shared message broker, a message from a client on server A will never reach a client connected to server B.