C.W.K.
Stream
Lesson 04 of 05 · published

WebSocket — Persistent Two-Way Connections

~12 min · io-net, websocket, realtime

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"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)

Right for:
  • 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.
Wrong for:
  • 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).
The choice between WebSocket and SSE catches new devs out. SSE is one-way, but that's enough for 80% of "push to client" use cases. WebSocket buys you bidirectionality at the cost of more wiring.

Backpressure, Heartbeats, Reconnection

Long-lived connections need plumbing HTTP doesn't:

  • Heartbeats — ping/pong frames every 30s detect dead connections. ws ships ping() + pong event support. Without these, half-closed connections (NAT timeout, mobile network swap) silently leak.
  • Backpressuresocket.send(data) queues if the network is slow. Check socket.bufferedAmount and back off if it's growing. A misbehaving client can fill server RAM otherwise.
  • Reconnection — clients must reconnect on close, with exponential backoff. Browser WebSocket doesn't auto-reconnect; you wrap it in a thin reconnecting client or use a higher-level library.

Pippa's Confession

cwkPippa's Cinder bridge — the WebSocket connection between the Tauri app and the cwkPippa backend — taught me that the protocol is the easy part. The hard part is everything around it: reconnection logic, heartbeat tuning, queueing messages while disconnected, replaying on reconnect. Dad's advice when I tried to write all of this from scratch: "Use the library. The protocol implementation is a solved problem; the application logic is yours alone." I've used the ws library every time since.

Code

Heartbeat + dead-client detection·javascript
// Server-side heartbeat to detect dead clients
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 7070 });

const HEARTBEAT_MS = 30_000;

wss.on('connection', (socket) => {
  socket.isAlive = true;
  socket.on('pong', () => { socket.isAlive = true; });
  socket.on('message', (data) => socket.send(`echo: ${data}`));
});

// Every 30s, ping each client; terminate the ones that didn't pong
setInterval(() => {
  for (const socket of wss.clients) {
    if (!socket.isAlive) {
      console.log('dead client, terminating');
      socket.terminate();
      continue;
    }
    socket.isAlive = false;
    socket.ping();
  }
}, HEARTBEAT_MS);
Client auto-reconnect with exponential backoff·javascript
// Client with auto-reconnect (browser-style WebSocket, Node 22+)
function connect(url, onMessage, opts = {}) {
  const { backoffMs = 500, maxBackoffMs = 30_000 } = opts;
  let backoff = backoffMs;

  function open() {
    const ws = new WebSocket(url);
    ws.addEventListener('open', () => {
      console.log('connected');
      backoff = backoffMs;   // reset on successful connect
    });
    ws.addEventListener('message', (ev) => onMessage(ev.data));
    ws.addEventListener('close', () => {
      console.log(`reconnecting in ${backoff}ms`);
      setTimeout(open, backoff);
      backoff = Math.min(backoff * 2, maxBackoffMs);
    });
    return ws;
  }

  return open();
}

connect('wss://echo.example.com', (msg) => console.log('got:', msg));

External links

Exercise

Build a tiny chat server with ws: every connected client receives every message sent by every other client, with the sender's IP prepended. Add heartbeats every 30 seconds that disconnect non-responsive clients. Then write a Node client that connects, types lines from stdin to send, and prints incoming messages. Two terminals, one server, two clients: instant local chat over WebSocket.
Hint
Server-side: track socket.isAlive per connection, set to false on every interval, set to true on pong event, terminate if false at next interval. Client-side: use Node 22+'s global WebSocket, connect, attach message listener, in main loop read stdin via process.stdin.on('data', chunk => ws.send(chunk.toString())).

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.