"HTTP is request/response. WebSocket is a phone call. Same TCP underneath, very different conversation shape."
What WebSocket Solves
HTTP works beautifully for "client asks, server answers, conversation done." It works badly for "server wants to push something to the client at an unpredictable moment." The historical workarounds — long-polling, server-sent events — all have downsides (overhead, one-way only, reconnection churn).
WebSocket is the protocol designed for this: after a one-time HTTP-based handshake, the same TCP socket becomes a bidirectional message-passing channel. Either side can send a message any time. The framing is small (a few bytes overhead per message). The connection stays open as long as both sides need it.
The Native ws Story (And Why You Use the Library)
Node ships node:ws as a *client* — Node 22+ has the global WebSocket class for client-side connections, matching the browser API:
const ws = new WebSocket('wss://echo.example.com');
ws.addEventListener('open', () => ws.send('hello'));
ws.addEventListener('message', (ev) => console.log(ev.data));
ws.addEventListener('close', () => console.log('done'));
For the *server* side, Node doesn't ship a built-in. The de facto library is ws (npm: ws) — battle-tested, used by Socket.IO underneath, by the LSP ecosystem, by Next.js's dev server. cwkPippa uses it for the Cinder bridge. Don't reinvent.
A Minimal Server
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 7070 });
wss.on('connection', (socket, req) => {
console.log('client connected from', req.socket.remoteAddress);
socket.on('message', (data) => {
// Broadcast to everyone
for (const client of wss.clients) {
if (client.readyState === 1) client.send(data.toString());
}
});
socket.on('close', () => console.log('client gone'));
});
That's a working chat server. Every client connects, every message goes to everyone. Real apps need rooms, auth, persistence — but the protocol primitives are this simple.
When WebSocket Is Right (And When It Isn't)
- Chat, presence, multiplayer games.
- Live dashboards (stock tickers, system metrics).
- Collaborative editing (Yjs over WebSocket).
- Backend → frontend push when SSE isn't enough.
- Bidirectional RPC over a single connection.
- One-way server → client streams (use Server-Sent Events — SSE — simpler protocol, plain HTTP, automatic reconnection).
- One-shot requests (use plain HTTP — no reason to maintain a persistent connection).
- Anything that needs caching by HTTP intermediaries (WebSocket messages aren't cacheable).
Backpressure, Heartbeats, Reconnection
Long-lived connections need plumbing HTTP doesn't:
- Heartbeats — ping/pong frames every 30s detect dead connections.
wsshipsping()+pongevent support. Without these, half-closed connections (NAT timeout, mobile network swap) silently leak. - Backpressure —
socket.send(data)queues if the network is slow. Checksocket.bufferedAmountand back off if it's growing. A misbehaving client can fill server RAM otherwise. - Reconnection — clients must reconnect on close, with exponential backoff. Browser
WebSocketdoesn't auto-reconnect; you wrap it in a thin reconnecting client or use a higher-level library.
Pippa's Confession
ws library every time since.