Most of the web still runs on a simple idea: the client asks, the server answers. That model works until it doesn't. And when it stops working, the instinct is to reach for WebSockets immediately.
That instinct is often right. But "often" is doing a lot of work in that sentence.
WebSockets flip the traditional request-response cycle by keeping a TCP connection alive between the client and server. Instead of the client polling every few seconds and hoping for fresh data, both sides can push messages whenever they have something to say. For chat apps, live dashboards, and collaborative editing tools, that difference is fundamental, not cosmetic.
But the gap between "WebSockets work" and "WebSockets work at scale" is wide, and most tutorials skip straight over it.
What Actually Changes When You Go Persistent
HTTP is stateless by design. Each request carries everything the server needs to respond, and then the connection closes. That simplicity is also its limitation: the server can't initiate contact. The client always has to ask first.
WebSockets change this by establishing a persistent, bidirectional channel after a one-time HTTP handshake (the protocol upgrade from http to ws). Once that handshake completes, you have a live pipe. The server can push data the moment something changes, without waiting to be asked.
Here is what that looks like with the ws library on Node.js 20+ LTS:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
console.log('Client connected');
socket.on('message', (data) => {
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(data.toString());
}
});
});
socket.on('close', () => {
console.log('Client disconnected');
});
});
That is a working broadcast server in under twenty lines. The ws library is lean, well-maintained, and gives you a production-ready foundation without pulling in a framework. Getting here is genuinely approachable.
The harder parts come next.
Where Implementations Actually Break Down
Authentication is not automatic. The WebSocket handshake is an HTTP request, which means you can authenticate during the upgrade phase using headers or cookies. But many implementations skip this entirely because the "hello world" version works without it. Once you add user-specific rooms or permissioned data, you need a real auth strategy baked in from the start, not retrofitted later.
Connection state lives in memory. A basic Node.js WebSocket server stores connected clients in a local Set. That works fine on a single instance. The moment you add a second server to handle load, those two instances have no idea about each other's clients. Broadcasts break. You need a shared pub/sub layer, typically Redis, to coordinate state across processes.
Idle connections accumulate. Mobile clients drop off networks without sending a close frame. Browsers go to sleep. Without a heartbeat mechanism (ping/pong frames at regular intervals), dead connections pile up silently and hold resources.
None of these are blockers. They are just problems that don't appear in tutorials and appear immediately in production.
When a Persistent Connection Is Actually Worth It
WebSockets are the right tool when you have genuinely bidirectional, low-latency communication needs. A multiplayer game, a trading dashboard, a live document editor. In these cases, polling is not just inefficient, it is architecturally wrong. You need push, not pull.
But plenty of "real-time" features do not actually require a persistent connection. A dashboard that refreshes every thirty seconds? Server-Sent Events (SSE) handles that with far less overhead, using a standard HTTP connection that the server can push to. A notification badge that updates when the user navigates? A short-lived fetch on visibility change is probably enough.
The persistent connection has a cost. Each open socket consumes memory and a file descriptor. Connection management, reconnection logic, and the pub/sub coordination layer all need to be built and maintained. That overhead is worth it when the use case justifies it. It is a liability when it does not.
When the Event Stream Grows Past What Your App Should Handle
If you build something that generates high-frequency event streams, like position updates in a map app, telemetry from connected devices, or user activity in a high-traffic collaborative tool, the volume of messages can grow faster than your application tier should absorb. Your Node.js server ends up doing event routing, persistence, and delivery simultaneously, and none of them get done well.
This is where separating your WebSocket server from your data infrastructure matters. Tools like Turboline are built specifically for ingesting and routing real-time event streams, so your application layer handles connection management without also becoming a message broker.
The Concrete Takeaway
Before you reach for WebSockets, ask one question: does the server need to initiate communication, or does the client just need fresh data occasionally? If it is the latter, start with SSE or a well-timed fetch. If the answer is genuinely yes, the server needs to push without being asked, then WebSockets are the right call. Set up ws, handle authentication during the upgrade, plan for a Redis-backed pub/sub layer before you need it, and implement heartbeats from day one. The foundation is simpler than it looks. The operational details are where the actual work lives.
Top comments (0)