Understanding WebSockets for real-time features
HTTP is request-response. The client asks, the server answers, the connection closes. This works for most of the web, but it falls apart when you need real-time updates: chat messages, live notifications, collaborative editing, stock tickers. Polling the server every second is wasteful. WebSockets solve this.
How WebSockets work
A WebSocket connection starts as an HTTP request (the "upgrade" handshake) and then switches to a persistent, bidirectional connection. Both the client and server can send messages at any time without the overhead of establishing a new connection each time.
HTTP: Client -> Request -> Server -> Response -> Connection closed
WebSocket: Client <-> Server (persistent, bidirectional)
sequenceDiagram
participant C as Client
participant S as Server
rect rgb(50, 50, 70)
note over C,S: HTTP (request-response)
C->>S: Request
S-->>C: Response
note over C,S: Connection closed
end
rect rgb(40, 70, 50)
note over C,S: WebSocket (persistent)
C->>S: Upgrade handshake
S-->>C: 101 Switching Protocols
C->>S: Message
S->>C: Message
S->>C: Message
C->>S: Message
note over C,S: Connection stays open
end
The connection stays open until either side closes it. Messages flow in both directions with minimal overhead (just a few bytes of framing per message, compared to hundreds of bytes of HTTP headers per request).
A practical example
Here is a minimal WebSocket server with Node.js using the ws library:
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => {
console.log("Client connected");
ws.on("message", (data) => {
const message = data.toString();
console.log("Received:", message);
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
ws.on("close", () => {
console.log("Client disconnected");
});
});And the client:
const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
ws.send("Hello from client");
};
ws.onmessage = (event) => {
console.log("Received:", event.data);
};
ws.onclose = () => {
console.log("Disconnected");
};That is a working chat server in about 20 lines per side.
When to use WebSockets
Chat and messaging. The classic use case. Messages need to arrive instantly without the client polling.
Live notifications. When something happens on the server (new order, deployment finished, build failed), push it to the client immediately.
Collaborative editing. Multiple users editing the same document need to see each other's changes in real time.
Live dashboards. Monitoring tools, analytics dashboards, and status pages that update without refreshing.
When NOT to use WebSockets
CRUD operations. Creating, reading, updating, and deleting data is perfectly served by REST. Adding WebSockets for a contact form is overengineering.
Infrequent updates. If data changes every few minutes, simple polling or Server-Sent Events (SSE) is simpler. WebSockets shine when updates are frequent and unpredictable.
Public APIs. WebSockets are harder to cache, proxy, and load balance than HTTP. For public-facing APIs, REST or GraphQL is more practical.
Server-Sent Events: the simpler alternative
If you only need server-to-client updates (no bidirectional communication), Server-Sent Events are simpler:
// Server (Express)
app.get("/events", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: new Date() })}\n\n`);
}, 1000);
req.on("close", () => clearInterval(interval));
});
// Client
const source = new EventSource("/events");
source.onmessage = (event) => {
console.log(JSON.parse(event.data));
};SSE works over regular HTTP, reconnects automatically, and is simpler to set up. Use it when the server needs to push updates but the client does not need to send messages back through the same channel.
Connection management
WebSocket connections need care:
Reconnection. Connections drop. Networks switch. Implement automatic reconnection with exponential backoff.
Heartbeats. Send periodic ping/pong messages to detect dead connections. Without this, you might not notice a client disconnected until you try to send it a message.
Authentication. Pass a token during the initial HTTP upgrade handshake. Do not rely on cookies alone for WebSocket authentication.
Sources
Related posts
Why I built Omnibase: a universal database MCP server
I got tired of copy-pasting query results between DataGrip and AI agents. So I built an MCP server that gives AI agents secure, direct access to any database.
Delta libraries: how diffing works and which library to use
What delta libraries do, how diff algorithms work under the hood, and a practical comparison of the most popular options in the JavaScript ecosystem.
Offline-first apps: harder than it sounds
Building apps that work without internet is one of those things that seems straightforward until you actually try it. Here is what makes it hard and how to approach it.
Enjoying the blog? Subscribe via RSS to get new posts in your reader.
Subscribe via RSS