WebSockets & Real-Time
Node.js Advanced
Chapter 3 of 8 · WebSockets & Real-Time
WebSockets & Real-Time
HTTP is half-duplex: the client asks, the server answers, and the connection closes. For real-time features — live notifications, chat, collaborative editing, live dashboards — that model breaks down. You either poll (wasteful and laggy) or you upgrade the connection to a full-duplex WebSocket: a persistent TCP tunnel where either side can push a frame at any time. Node's single-threaded, event-driven architecture is a natural fit for holding thousands of these long-lived connections simultaneously.
HTTP vs WebSocket vs SSE
| Mechanism | Direction | Connection | Best for | Overhead |
|---|---|---|---|---|
| HTTP polling | Client → Server only | New TCP per request | Infrequent checks (every 30 s+) | High (full HTTP headers each time) |
| SSE (Server-Sent Events) |
Server → Client only | Persistent HTTP/1.1 stream | Push feeds (news, notifications, progress) | Low; auto-reconnect built in |
| WebSocket | Full-duplex (both ways) | Persistent TCP after upgrade | Chat, games, collaborative tools, live data | Very low (2–10 byte frame header) |
The WebSocket Handshake
The ws Library — Server Basics
Connection State — readyState
Broadcasting to All Connected Clients
Storing Metadata on Connections
The ws library's WebSocket objects are plain objects — you can attach any properties you need to track per-connection state (username, rooms, authenticated status).
Rooms — Scoped Broadcasting
Heartbeat — Detecting Dead Connections
TCP connections can die silently: the client's laptop closes the lid, a proxy drops the socket, the network blips. From the server's perspective the socket looks open but nothing will ever arrive. A ping/pong heartbeat detects these zombie connections and terminates them.
Message Protocol — Typed JSON Envelopes
Raw strings over WebSocket become unmanageable fast. A simple typed envelope makes the server's message-dispatch loop clean and extensible.
WebSocket Client in Node.js
ws.close() vs ws.terminate()
ws.close(code, reason) sends a WebSocket close frame — a clean handshake. The peer gets to send its own close frame and flush pending messages.
ws.terminate() destroys the underlying TCP socket immediately — no close frame, no handshake. Use it for zombie connections (heartbeat failures) or misbehaving clients.
Binary vs Text Frames
WebSocket frames are either text (UTF-8) or binary. ws.send(string) sends text; ws.send(Buffer) sends binary. The isBinary flag in the message event tells you which arrived.
For JSON APIs always use text. For audio/video/file payloads, binary avoids the base64 overhead of encoding binary data into a JSON string.
Scaling Beyond One Process
wss.clients only contains sockets on this process. If you cluster or run multiple instances, broadcast messages won't reach clients connected to other workers.
Solution: use a pub/sub bus (Redis Pub/Sub is the classic choice) — each worker subscribes to a channel and re-broadcasts locally to its own clients.
WebSocket Close Codes
1000 — Normal closure. 1001 — Going away (server restart). 1006 — Abnormal closure (connection lost without close frame). 1011 — Server error.
App-defined codes live in 4000–4999: 4001 Unauthorized, 4002 Bad Request, 4003 Rate limited — these are yours to define freely.
⚠️ Back-Pressure on ws.send()
If you send() faster than the client can consume, frames queue up in the kernel's TCP send buffer. When the buffer fills, the OS blocks writes — and in Node's non-blocking model this means pending writes pile up in memory on your side.
Check ws.bufferedAmount before sending high-frequency data (e.g. live chart ticks). If it's non-zero, you're sending faster than the client is draining — skip the frame or throttle the producer rather than letting the queue grow.
💡 ws vs socket.io
ws is a thin, spec-compliant WebSocket library — no extras, full control. You implement rooms, heartbeats, acknowledgements, and reconnection logic yourself (as shown above).
socket.io wraps ws and adds all of those features out of the box — plus a fallback to long-polling for environments that block WebSockets. The cost is a custom protocol: the client must use the socket.io client library; bare WebSocket clients can't talk to it directly.
For internal microservice communication or when you control both ends, ws is preferable. For public-facing apps where you want reconnection/rooms/acks without building them, socket.io saves time.
Coding Challenges
Challenge 1 — Echo Server with Heartbeat
Build a WebSocket server that: echoes every text message back with a timestamp prepended ([10:45:32] your message), runs a heartbeat every 10 seconds and terminates zombie connections, tracks total messages received (all clients combined) and broadcasts a { type: "stats", count: N } JSON frame to all clients every 30 seconds. Write a Node.js ws client that connects, sends 5 messages one second apart, logs every frame it receives, then disconnects cleanly.
Challenge 2 — Multi-Room Chat Server
Build a chat server that supports joining and leaving named rooms, broadcasting messages within a room, and a { type: "who", room } query that returns the list of usernames in that room. On connect, extract the username query parameter and reject the connection (close code 4001) if it's missing or empty. On disconnect, broadcast a { type: "leave", username } frame to any room the user was in. Include a small test script that spins up 3 clients, puts them in two overlapping rooms, sends messages, and verifies the right clients receive them.
Challenge 3 — Live Dashboard Feed
Build a server that generates fake "sensor readings" (a random number between 0–100 every 500 ms) and streams them only to subscribed clients. Clients subscribe by sending { type: "subscribe", sensor: "temp" } and unsubscribe with { type: "unsubscribe", sensor: "temp" }. A client should be able to subscribe to multiple sensors simultaneously. The server must check ws.bufferedAmount before each send and skip the frame (log a warning instead) if it is non-zero — demonstrating back-pressure awareness. Include a test that connects 5 clients with different subscription sets and verifies each receives only its subscribed sensors.