WebSockets give you a persistent, bidirectional connection between client and server, and most developers reach for them before checking whether their actual use case even needs bidirectionality.
A WebSocket is a long-lived TCP connection, upgraded from an initial HTTP request, that lets both client and server send messages to each other at any time without the request/response overhead of repeated HTTP calls. It's the right tool for chat apps, live collaboration, and multiplayer features — anything where the server genuinely needs to push data to the client unprompted, and the client also needs to send frequent updates back.
Why WebSockets Matter (and When to Skip Them)
Polling wastes bandwidth and adds latency (you only find out about an update on your next poll interval). Server-Sent Events (SSE) solve server-to-client push elegantly but are one-directional — the client can't send data back over the same connection. WebSockets solve both directions at once, which is exactly the point, and exactly the added complexity you're paying for.
Skip WebSockets if you only need server-to-client updates (use SSE — simpler, works over plain HTTP, auto-reconnects) or if updates are infrequent enough that polling every few seconds is genuinely fine. Reach for WebSockets specifically when the client needs to send frequent messages too.
Getting Started with WebSockets
A minimal WebSocket server using the ws library:
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
socket.on("message", (data) => {
const message = JSON.parse(data.toString());
// broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(JSON.stringify(message));
}
});
});
});
Client side, using the native browser API:
const socket = new WebSocket("wss://example.com/ws");
socket.onopen = () => socket.send(JSON.stringify({ type: "join", room: "general" }));
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log("Received:", message);
};
Core WebSocket Concepts Every Developer Should Know
Connections don't survive network changes or server restarts — reconnection logic isn't optional, it's required for any production use:
function connectWithRetry(url: string, onMessage: (data: unknown) => void) {
const socket = new WebSocket(url);
socket.onmessage = (e) => onMessage(JSON.parse(e.data));
socket.onclose = () => {
setTimeout(() => connectWithRetry(url, onMessage), 2000); // exponential backoff in practice
};
return socket;
}
Message typing matters as much as REST endpoint typing. Untyped JSON.parse(data) on both ends is where WebSocket bugs come from — define a discriminated union for message types and validate incoming messages against it, the same way you'd validate a REST request body.
type WSMessage =
| { type: "chat"; text: string; userId: string }
| { type: "typing"; userId: string }
| { type: "presence"; userId: string; online: boolean };
Scaling beyond one server node requires a shared pub/sub layer. A single WebSocket server can't broadcast to clients connected to a different server instance — Redis pub/sub (or a managed service) is the standard fix, relaying messages between server instances so broadcasts reach every connected client regardless of which node they're on.
Heartbeats detect dead connections. TCP doesn't always notice a connection dropped (e.g., a laptop closing its lid) — periodic ping/pong frames let the server clean up stale connections proactively.
Common WebSocket Mistakes and How to Fix Them
Mistake 1: no reconnection logic on the client. A dropped connection (network blip, server deploy) silently ends real-time updates until the user refreshes. Fix: implement automatic reconnect with exponential backoff, and surface connection state in the UI.
Mistake 2: broadcasting to all clients instead of scoping by room/channel. Sending every message to every connected client wastes bandwidth and leaks data across unrelated sessions. Fix: track room/channel membership server-side and only broadcast to relevant subscribers.
Mistake 3: skipping authentication on the WebSocket handshake. Unlike REST requests, WebSocket connections are easy to forget to authenticate, since the initial HTTP upgrade request can slip past middleware assumptions. Fix: validate an auth token (via query param or initial handshake message) before accepting the connection.
When Should You Use WebSockets Instead of SSE or Polling?
Use WebSockets for bidirectional real-time needs — chat, collaborative editing, multiplayer games. Use SSE for server-to-client-only updates like live notifications or dashboards. Use polling for infrequent updates where real-time isn't actually a requirement, since it's the simplest to implement and debug.
WebSockets in Production
Modern serverless platforms now support WebSockets natively (Vercel Functions with Fluid Compute, Cloudflare Durable Objects) without needing a separate always-on WebSocket server — worth checking before defaulting to a dedicated Node process. Also monitor connection count and message throughput explicitly; WebSocket servers fail in ways HTTP servers don't (slow memory leaks from unclosed connections) that standard HTTP monitoring won't catch.
Before reaching for a WebSocket, check if the client actually needs to send frequent messages back — if not, SSE gets you real-time updates with a fraction of the operational complexity.