What Is WebSocket? The Technology Behind Real-Time Applications
You send a message on WhatsApp and the other person sees it instantly. Prices in a stock trading app update continuously. In a multiplayer game, your opponent's every move appears on your screen in real time.
Behind all of these is the same question: how does the server notify the client when something happens? With HTTP, solving this problem turns out to be much harder than expected.
HTTP's Real-Time Problem
HTTP works on a request-response model. The client asks, the server answers. The server can't send anything to the client on its own — it always has to wait for the client to ask.
The first solution developed for real-time data needs was polling: the client would ask the server at regular intervals "is there anything new?"
// Bad solution: Polling setInterval(async () => { const response = await fetch("/api/messages/new"); const messages = await response.json(); if (messages.length > 0) displayMessages(messages); }, 1000); // Ask every second
The problem is obvious: with 10,000 users, that's 10,000 requests per second. Most responses are empty. Server under load, high latency, wasted bandwidth.
Long polling was smarter: the client sends a request, the server holds the connection open until new data arrives, responds when data comes, and the client immediately sends a new request.
// Better but still not ideal: Long Polling async function longPoll() { const response = await fetch("/api/messages/wait"); const messages = await response.json(); displayMessages(messages); longPoll(); // Immediately send new request }
Fewer empty responses, but still a new HTTP connection for every message. The TCP handshake cost is paid every time.
WebSocket: A Persistent Connection
WebSocket establishes a single persistent connection between client and server. Once the connection is established, either side can send data whenever it wants. No extra overhead, no polling, no repeatedly establishing connections.
A WebSocket connection starts with the HTTP upgrade mechanism:
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
If the server accepts:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 — Switching Protocols. This connection no longer runs on HTTP, it runs on the WebSocket protocol. Either side can send messages whenever it wants.
Using WebSocket in the Browser
// Establish connection const ws = new WebSocket("wss://example.com/chat"); // When connection opens ws.addEventListener("open", () => { console.log("Connection established"); ws.send(JSON.stringify({ type: "join", room: "general", username: "john" })); }); // When message arrives ws.addEventListener("message", (event) => { const data = JSON.parse(event.data); switch (data.type) { case "message": displayMessage(data); break; case "user_joined": showNotification(`${data.username} joined the room`); break; case "typing": showTypingIndicator(data.username); break; } }); // When connection closes ws.addEventListener("close", (event) => { console.log(`Connection closed: ${event.code} - ${event.reason}`); setTimeout(reconnect, 3000); }); // Send a message function sendMessage(text) { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "message", text, timestamp: Date.now() })); } }
WebSocket Server with Node.js
import { WebSocketServer } from "ws"; const wss = new WebSocketServer({ port: 8080 }); const rooms = new Map(); wss.on("connection", (ws) => { ws.isAlive = true; ws.on("message", (rawData) => { const data = JSON.parse(rawData); switch (data.type) { case "join": handleJoin(ws, data); break; case "message": handleMessage(ws, data); break; case "typing": handleTyping(ws, data); break; } }); ws.on("close", () => handleDisconnect(ws)); ws.on("pong", () => { ws.isAlive = true; }); }); function handleJoin(ws, data) { ws.username = data.username; ws.room = data.room; if (!rooms.has(data.room)) rooms.set(data.room, new Set()); rooms.get(data.room).add(ws); broadcast(data.room, { type: "user_joined", username: data.username }, ws); } function handleMessage(ws, data) { broadcast(ws.room, { type: "message", username: ws.username, text: data.text, timestamp: Date.now() }); } function broadcast(room, data, exclude = null) { const clients = rooms.get(room) || new Set(); const message = JSON.stringify(data); clients.forEach(client => { if (client !== exclude && client.readyState === 1) { client.send(message); } }); } // Heartbeat: clean up dead connections setInterval(() => { wss.clients.forEach(ws => { if (!ws.isAlive) return ws.terminate(); ws.isAlive = false; ws.ping(); }); }, 30000);
Scaling: Multiple Servers
When a single server isn't enough, a problem emerges: a user connected to server A can't message a user connected to server B. The solution: a pub/sub system.
import { createClient } from "redis"; const publisher = createClient(); const subscriber = createClient(); await publisher.connect(); await subscriber.connect(); function handleMessage(ws, data) { publisher.publish("chat:general", JSON.stringify({ type: "message", username: ws.username, text: data.text })); } subscriber.subscribe("chat:general", (message) => { localClients.forEach(client => { if (client.readyState === 1) client.send(message); }); });
Every server listens to the Redis channel. Regardless of which server a user is connected to, all users receive the message.
WebSocket vs Server-Sent Events
WebSocket isn't always the right tool. Server-Sent Events (SSE) is a simpler alternative for one-way data flow from server to client.
SSE works over standard HTTP, supports automatic reconnect, and has excellent browser support. But sending data from client to server requires a separate HTTP request.
Use WebSocket: Bidirectional real-time communication. Chat, games, collaboration tools.
Use SSE: One-way streaming. Live notifications, news feed, progress bars, log streaming.
Understanding WebSocket is understanding how real-time systems work. From chat to live collaboration tools, from games to financial platforms — the power of a persistent connection is everywhere.