WebSockets & Real-Time Communication
WebSockets & Real-Time Communication
LinkedIn Hook
HTTP is great for asking questions. But what happens when the server needs to tell YOU something?
You could poll — ask the server every second: "Anything new?" That works. It's also wasteful and slow.
Or you could use WebSockets — hold the line open and let the server shout whenever something changes.
This is why Slack messages appear instantly. Why stock tickers update in real time. Why your game registers your move without refresh.
Lesson 8.3 of my Networking Interview Prep series covers: how WebSocket connections are established, what full-duplex means, when to use WebSockets vs Server-Sent Events, and the most common interview questions on real-time communication.
Read the full lesson → [link]
#WebSockets #RealTime #WebDevelopment #InterviewPrep #Networking #BackendDevelopment
What You'll Learn
- Why HTTP's request-response model breaks down for real-time applications
- How WebSocket connections are established via an HTTP upgrade handshake
- What full-duplex means and why it matters
- How to implement WebSocket client and server in JavaScript
- When to use WebSockets, Server-Sent Events (SSE), or Long Polling
- What the WebSocket frame format looks like under the hood
The Telephone vs Letter Analogy
HTTP works like sending letters. You write a letter (request), mail it, and wait for a reply (response). The conversation is always client-initiated. If the post office (server) discovers something important, it cannot contact you — it has to wait for your next letter.
WebSockets work like a telephone call. Once you dial (handshake), both sides have a live, two-way channel. Either side can speak at any moment. The server can push a message without waiting for a request.
For letter exchanges, HTTP is perfect. For conversations, you need a telephone.
The Problem with HTTP for Real-Time
HTTP is a half-duplex, request-driven protocol. Every exchange is initiated by the client. The server can only respond — it cannot spontaneously push data.
This breaks down for real-time scenarios:
Chat application with HTTP polling:
Client: "Any new messages?" → Server: "No"
Client: "Any new messages?" → Server: "No"
Client: "Any new messages?" → Server: "No"
Client: "Any new messages?" → Server: "Yes! Here's a message"
To simulate real-time behavior with HTTP, clients must poll — repeatedly ask the server if anything changed. This is wasteful:
- New TCP connection (or connection reuse) per poll
- Headers sent on every request (hundreds of bytes of overhead)
- High latency if polling interval is too long
- Unnecessary server load if polling interval is too short
For a chat app with 10,000 users polling every second, that is 10,000 HTTP requests per second for mostly empty responses.
What WebSockets Solve
WebSocket provides a persistent, full-duplex communication channel over a single TCP connection.
- Persistent: connection stays open as long as both sides want it
- Full-duplex: client and server can send messages simultaneously, without waiting for the other
- Low overhead: after the initial handshake, WebSocket frames have only 2–14 bytes of header (vs. hundreds for HTTP)
- Server push: the server can initiate a message at any time
Chat application with WebSockets:
[One-time handshake]
Client → Server: "Upgrade to WebSocket"
Server → Client: "101 Switching Protocols"
[Persistent connection open]
Client → Server: "Hello!" (instant, no headers)
Server → Client: "Hi there!" (server-initiated, instant)
Server → Client: "User X just joined" (server-initiated, no request needed)
Client → Server: "Goodbye"
[Either side closes]
The WebSocket Handshake
WebSocket connections start as HTTP — they use a special HTTP upgrade mechanism:
Client sends (HTTP GET with upgrade headers):
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Server responds (if it accepts):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 Switching Protocols is the only status code that says "the protocol being used on this connection is changing." After this response, HTTP is gone — the connection is now a WebSocket channel.
Sec-WebSocket-Key / Sec-WebSocket-Accept: the client sends a random base64 key; the server concatenates it with a magic GUID, SHA-1 hashes the result, and base64 encodes it. This confirms the server intentionally performed the upgrade (prevents malicious proxies from creating WebSocket connections accidentally).
After the handshake:
- Communication is on the same TCP connection that carried the HTTP upgrade
- The connection runs on port 80 (ws://) or 443 (wss://)
- wss (WebSocket Secure) runs WebSocket over TLS, just like HTTPS
The WebSocket Frame Format
Once established, data travels in frames — not HTTP headers/bodies. A minimal frame:
WebSocket Frame Header (2-14 bytes):
Bit: 0 7 8 15 16 31
+-+-+-+-+-------+-+-------------+...--------+
|F|R|R|R| opcode|M| Payload len | Extended |
|I|S|S|S| |A| (7 bits) | length... |
|N|V|V|V| |S| | |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+...--------+
Opcode meanings:
0x0 = Continuation frame
0x1 = Text frame (UTF-8)
0x2 = Binary frame
0x8 = Connection Close
0x9 = Ping
0xA = Pong
For a "Hello" message, the WebSocket frame overhead is 6 bytes (for client-to-server, masking adds 4 bytes). Compare to HTTP where "Hello" would carry hundreds of bytes of headers.
Building a WebSocket Server and Client
// Server — Node.js with 'ws' library
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
wss.on("connection", (socket, request) => {
console.log("Client connected:", request.socket.remoteAddress);
// Send a message to this client
socket.send(JSON.stringify({ type: "welcome", message: "Connected!" }));
// Receive messages from this client
socket.on("message", (data) => {
const parsed = JSON.parse(data.toString());
console.log("Received:", parsed);
// Broadcast to ALL connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: "chat",
from: parsed.username,
message: parsed.message,
timestamp: Date.now()
}));
}
});
});
socket.on("close", (code, reason) => {
console.log("Client disconnected:", code, reason.toString());
});
socket.on("error", (err) => {
console.error("Socket error:", err.message);
});
});
// Client — Browser
const socket = new WebSocket("wss://example.com/chat");
// Connection opened
socket.addEventListener("open", () => {
console.log("Connected to server");
socket.send(JSON.stringify({
type: "join",
username: "Rakibul"
}));
});
// Receive messages from server (server-initiated!)
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
if (data.type === "chat") {
displayMessage(data.from, data.message);
}
});
// Send a message when user presses Enter
function sendMessage(text) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: "chat",
username: "Rakibul",
message: text
}));
}
}
// Connection closed
socket.addEventListener("close", (event) => {
console.log("Disconnected:", event.code, event.reason);
// Reconnect logic here if needed
});
WebSocket Connection States
The browser WebSocket API exposes four readyState values:
WebSocket.CONNECTING // 0 — handshake in progress
WebSocket.OPEN // 1 — connection established, can send/receive
WebSocket.CLOSING // 2 — close handshake initiated
WebSocket.CLOSED // 3 — connection fully closed
Always check readyState === WebSocket.OPEN before calling .send(). Calling send on a closed socket throws an error.
Alternatives to WebSockets
WebSockets are not always the right tool. Two simpler alternatives exist for specific patterns:
Server-Sent Events (SSE)
SSE is a unidirectional channel — server to client only. The client opens one long-lived HTTP connection and the server streams events down it.
// Server — SSE with Node.js / Express
app.get("/events", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// Send an event every 2 seconds
const interval = setInterval(() => {
const data = JSON.stringify({ price: 142.50, time: Date.now() });
res.write(`event: price-update\n`);
res.write(`data: ${data}\n\n`); // double newline ends the event
}, 2000);
req.on("close", () => clearInterval(interval));
});
// Client — SSE is built into browsers
const source = new EventSource("/events");
source.addEventListener("price-update", (event) => {
const data = JSON.parse(event.data);
updateStockTicker(data.price);
});
source.onerror = () => {
console.log("SSE connection lost — browser auto-reconnects");
};
SSE auto-reconnects on disconnect (browser handles it). No special library needed. Works perfectly for one-way server→client streams.
Long Polling
The client sends a request and the server holds it open until something happens (or a timeout):
// Long polling — client keeps asking, server delays response until data exists
async function longPoll() {
while (true) {
try {
const res = await fetch("/api/notifications/poll");
// Server held this request for up to 30s waiting for a notification
const notifications = await res.json();
processNotifications(notifications);
} catch (err) {
await new Promise(resolve => setTimeout(resolve, 1000)); // retry after 1s
}
}
}
Long polling works everywhere HTTP works (no WebSocket support needed). But it has higher overhead than WebSockets and SSE for frequent updates.
Choosing the Right Real-Time Technology
| Scenario | Best Choice | Why |
|---|---|---|
| Chat app, multiplayer game, trading platform | WebSocket | Full-duplex, low overhead, high message frequency |
| Live feed, notifications, dashboards | SSE | Server→client only, simpler, auto-reconnect |
| Simple notifications, legacy browser support | Long Polling | Works everywhere, no protocol switch |
| Infrequent updates (every few minutes) | Short Polling | Simplest; low frequency makes overhead acceptable |
Common Mistakes
-
Forgetting to handle disconnections and reconnect. WebSocket connections drop — network blips, server restarts, mobile apps going to background. A production WebSocket client needs reconnection logic with exponential backoff. The browser does not auto-reconnect WebSockets (SSE does, but WebSocket does not).
-
Scaling WebSockets without a message broker. A standard WebSocket server only broadcasts to clients connected to that specific server instance. If you have 3 server instances and a message arrives at instance #1, clients connected to instances #2 and #3 never see it. Fix: use a pub/sub broker (Redis Pub/Sub, Kafka) as a shared message bus. Every instance subscribes and rebroadcasts to its local clients.
-
Choosing WebSockets when SSE is sufficient. WebSockets require both client and server to implement the full bidirectional protocol. If your use case is server-only broadcasts (stock prices, news feeds, live logs), SSE is simpler, uses standard HTTP, works through most proxies, and auto-reconnects. Reach for WebSockets only when you need client-to-server messages over the same persistent channel.
Interview Questions
Q: How is a WebSocket connection established?
A WebSocket connection starts as a regular HTTP GET request with special headers:
Upgrade: websocketandConnection: Upgrade. The server responds with101 Switching Protocols. After that exchange, the TCP connection that carried HTTP is repurposed as a WebSocket channel — HTTP headers are no longer used, and data flows as lightweight binary frames. WebSocket runs on port 80 (ws://) or 443 (wss:// over TLS), same as HTTP/HTTPS.
Q: What is the difference between WebSockets and Server-Sent Events?
WebSocket is full-duplex: either side (client or server) can send messages at any time. SSE is unidirectional: the server streams events to the client, but the client cannot push data back over the same channel. SSE is built on plain HTTP (persistent chunked response), requires no protocol switch, auto-reconnects on disconnect, and is simpler to implement. Use WebSockets when clients need to send messages frequently (chat, gaming). Use SSE when only the server sends (live feeds, notifications).
Q: How do you scale WebSockets across multiple server instances?
When multiple server instances handle WebSocket connections, messages received by instance A cannot directly reach clients connected to instances B or C. The solution is a message broker (Redis Pub/Sub, Kafka, RabbitMQ). When instance A needs to broadcast, it publishes to the broker. All instances subscribe to the broker and rebroadcast to their locally connected clients. This way, every message reaches every client regardless of which server they are connected to. A sticky load balancer (session affinity) also helps — routing a client's connections to the same instance — but is not sufficient on its own for broadcast scenarios.
Q: What does readyState === WebSocket.OPEN mean and why check it?
The WebSocket
readyStateproperty reflects the current connection state: CONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3). Calling.send()on a socket that is not in the OPEN state throws anInvalidStateError. Network failures, server restarts, or application code can close the connection at any point. Always checkreadyState === WebSocket.OPENbefore calling.send(), and implement reconnection logic to handle transitions to CLOSED.
Quick Reference — Cheat Sheet
WebSocket Handshake
Client → Server:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <random base64>
Sec-WebSocket-Version: 13
Server → Client:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <derived key>
[TCP connection now speaks WebSocket protocol]
WebSocket vs SSE vs Long Polling
| Feature | WebSocket | SSE | Long Polling |
|---|---|---|---|
| Direction | Full-duplex | Server→Client | Client-initiated |
| Protocol | ws:// / wss:// | HTTP | HTTP |
| Header overhead | 2-14 bytes/frame | Low (chunked) | High per request |
| Auto-reconnect | No | Yes | Manual |
| Browser support | All modern | All modern | All |
| Proxy friendly | Sometimes issues | Always | Always |
| Best for | Chat, gaming, trading | Feeds, notifications | Simple notifications |
WebSocket Close Codes
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 1001 | Endpoint going away (server restart) |
| 1006 | Abnormal closure (connection dropped) |
| 1008 | Policy violation |
| 1011 | Internal server error |
Client-Side WebSocket Skeleton
const socket = new WebSocket("wss://host/path");
socket.onopen = () => socket.send(JSON.stringify({ type: "hello" }));
socket.onmessage = (e) => console.log(JSON.parse(e.data));
socket.onclose = (e) => reconnect(e.code);
socket.onerror = (e) => console.error(e);
// Check state before sending
if (socket.readyState === WebSocket.OPEN) {
socket.send(data);
}
Previous: Lesson 8.2 → Next: Lesson 8.4 →
This is Lesson 8.3 of the Networking Interview Prep Course — 8 chapters, 32 lessons.