WebSockets give us a persistent, full-duplex channel between browser and server. Once the connection is open, either side can send data anytime — no request needed.
In simple language: HTTP is like sending letters, WebSockets is like opening a phone line.
Why Not Just Use HTTP?
HTTP is request/response. The client asks, the server replies. The server can’t push data on its own. Workarounds (polling, long polling) are wasteful or laggy.
For chat apps, live dashboards, multiplayer games, collaborative editors — we need real two-way communication. That’s WebSockets.
The HTTP Upgrade Handshake
A WebSocket starts life as a regular HTTP request that asks to “upgrade” the connection.
Client request:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The 101 Switching Protocols status is the moment HTTP steps aside. After this, the same TCP socket carries WebSocket frames instead of HTTP.
URLs use ws:// (plain) or wss:// (over TLS). Use wss:// in production — it gives the same security as HTTPS.
Frames, Not Requests
After upgrade, data flows as small frames. Each frame has:
- An opcode (text, binary, close, ping, pong).
- A payload length.
- A masking key (client → server frames are masked).
- The payload itself.
This is much lighter than HTTP — no headers per message, no method line, no status code.
Ping / Pong (Keepalive)
WebSockets have a built-in heartbeat. The server can send a ping frame; the client must reply with a pong. This:
- Detects dead connections (router rebooted, NAT entry expired).
- Keeps middleboxes from closing idle connections.
Most libraries do this automatically every 30-60 seconds.
Client Example
// Open a WebSocket
const ws = new WebSocket("wss://chat.example.com/room/42");
ws.addEventListener("open", () => {
console.log("connected");
ws.send(JSON.stringify({ type: "join", user: "manish" }));
});
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
console.log("from server:", msg);
});
ws.addEventListener("close", (event) => {
console.log("closed", event.code, event.reason);
// Reconnect logic goes here — WebSockets don't auto-reconnect
});
ws.addEventListener("error", (err) => {
console.error("ws error", err);
});
// Send any time
function sendChat(text) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "chat", text }));
}
}
Server Example (Node)
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
socket.send(JSON.stringify({ type: "welcome" }));
socket.on("message", (raw) => {
const msg = JSON.parse(raw);
// Broadcast to everyone
for (const client of wss.clients) {
if (client.readyState === 1) client.send(raw);
}
});
});
When to Use What
- WebSockets — bidirectional and frequent (chat, games, live editing).
- SSE (Server-Sent Events) — server → client only, simpler, auto-reconnect.
- Long polling — fallback for environments where WebSockets are blocked.
- Short polling — quick & dirty, low traffic, no infra changes.
If only the server pushes updates, prefer SSE. If both sides talk, go WebSockets.
Common Gotchas
- No auto-reconnect — write reconnection logic with exponential backoff.
- Behind proxies — older proxies/load balancers may not handle the Upgrade. Modern ones (nginx, Caddy, Cloudflare) do, but check.
- Authentication — we can’t easily add custom headers in browsers. Common pattern: pass a short-lived token in the URL query string and validate on connect.
- Scaling — connections are sticky. Scaling to N servers requires a pub/sub layer (Redis, NATS) so messages reach clients connected to other nodes.
Interview Tip
The key concept is the Upgrade handshake: WebSockets start as an HTTP request and switch protocols mid-flight. Mention 101 Switching Protocols and you’ve shown you actually understand the wire-level handoff.