DEV Community

Yaseen Khatib
Yaseen Khatib

Posted on Originally published at yaseenkhatib.streamerosai.com

WebSocket Telemetry at Scale: When One Process Isn't Enough

[ EXECUTIVE TEARDOWN // TL;DR ]

  • In-memory broadcast only reaches the local process — multi-instance real-time needs a shared backplane (Redis pub/sub).
  • Scope subscriptions into rooms so each client receives only the streams it watches, collapsing bandwidth and CPU.
  • Coalesce high-frequency sources on the server to a renderable frame rate before broadcasting.
  • Handle backpressure at the source, not after it has already flooded the client.

A single WebSocket server streaming telemetry to a few clients is a weekend project. Feeding thousands across multiple instances is systems work. I learned that on streamerOS: the “one process holds every socket” sketch sailed on one pod and face-planted the moment we scaled out. Each instance owns its own connection table, so A cannot fan out to subscribers that landed on B; broadcasts die at the process boundary and your graphs freeze.

The horizontal-scaling problem

Once you run more than one server process behind a load balancer, connections fan out across instances. An event that originates on one instance has to reach subscribers on all of them. In-memory broadcast only reaches the local process; you need a shared backplane so every instance hears every event. In the pattern I call Trinity Architecture, this cross-instance fanout lives in the orchestration tier — the UI stays dumb, render-only, and the adapter keeps payloads lean. I use a Serialization Adapter there too; on IntegrateX it stripped non-essential React Flow UI metadata before persistence and cut graph payloads 94%, and the same habit keeps telemetry frames tight.

scale.ts

// Redis pub/sub backplane → broadcast across every instance
import { createAdapter } from "@socket.io/redis-adapter";

io.adapter(createAdapter(pubClient, subClient));

// emit anywhere; every instance's subscribers receive it
metrics.on("tick", (m) => io.to("telemetry").emit("metric", m));
Enter fullscreen mode Exit fullscreen mode

Rooms, not broadcasts

Do not push every metric to every client. Scope subscriptions into rooms — per dashboard, per device, per tenant — so a client receives only the streams it is watching. This collapses bandwidth and CPU from O(clients × events) toward O(interested clients), which is the difference between a system that scales and one that melts at a few thousand connections. On streamerOS, moving from a global channel to roomed feeds stopped NIC spikes, trimmed CPU interrupts, and cut render thrash when operators flipped between views; the UI joined/left rooms via the orchestrator in my Trinity split, never from components.

Coalesce at the source

High-frequency sources emit faster than any client can render or any socket should carry. Sample or debounce on the server to a sane frame rate before broadcasting, so you push the latest truth at a rate the wire and the React reconciler can actually absorb. Budget is ~16ms at 60fps; a 200 Hz stream will starve the UI thread. Track last-value-wins per key and pre-aggregate counters server-side. Backpressure handled at the source beats backpressure discovered at the client.

Scaling real-time is not about a faster socket. It is about making sure each event travels exactly as far as it needs to — and no further.

For the single-server fundamentals and the React consumption side, see Real-Time Telemetry; the live systems are streamerOS and the CMZ portal. The instinct I bring to a team is deciding how far each event should travel before you scale out, not after the graphs freeze.

~/keep-reading

YK

Yaseen Khatib · MERN + AI Architect

Ships autonomous AI products solo — five in the last twelve months. More about Yaseen →

Need an engineer who can build this?

I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.

Get in touch →See what I've shipped


Originally published at yaseenkhatib.streamerosai.com/blog/websocket-telemetry-at-scale/.

Top comments (3)

Collapse
 
raknaos profile image
Raknaos •

The process-boundary broadcast failure is the classic trap because it works perfectly in dev -- one pod, one connection table, every demo green -- and the graphs only freeze after you scale out, when the person debugging is three layers removed from the change.

The backpressure point is underrated. Most implementations coalesce at the client with requestAnimationFrame, which means every instance still pays full fan-out bandwidth for frames nobody renders. Coalescing server-side to a renderable rate (say 10-15 Hz per room) cuts the backplane traffic by the same factor and keeps slow clients from dragging the whole room. Did you end up sharding rooms across instances by hash, or does every instance subscribe to every stream over the backplane? The second is simpler until room count makes the pub/sub channel fan-out its own bottleneck.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.