Concluded·3 Aug 2026 · 5 min read

A hundred remote sites into one broker, and the measurement that reversed itself

Redpanda v24.2.18 (single node, 2 cores, 2 GB) · kafkajs · Docker Compose · Proxmox LXC

Two questions come up whenever a fleet grows past a handful of sites. Will one cluster carry it, and what happens to the other sites when one goes wrong.

I built a hundred sites to find out, and got the second answer wrong before I got it right.

The rig

BrokerRedpanda v24.2.18, single node, 2 cores, 2 GB
Estate100 sites × 50 tags, published every second
Sitesinternational place names — rotterdam-01, houston-01, seville-02
Strategiesone topic with 100 partitions, and one topic per site

Two different things are measured, and conflating them is how the wrong answer happens:

  • publish latency — how long the producing client waited for an ack
  • end-to-end lag — from a reading being produced at a site to a central consumer actually seeing it

Only the second can tell you anything about blast radius, which is why the rig has a separate consumer recording per-site percentiles.

Experiment 1 — capacity

Messages295,000 in 61.5 s
Rate4,797 msg/sec
Errors0
Publish latencyp50 58 ms · p95 65 ms · p99 119 ms
End-to-end lagp50 82 ms · p95 98 ms · p99 164 ms
Sites seen100 / 100

THE ESTATE

100 REMOTE SITES · 50 TAGS EACHONE BROKER2 cores · 2 GB4,797/szero errors98 mssite to centre, p95
One hundred sites, fifty tags each, every second. A two-core broker took the lot without complaint.

Dull, in the good way. Two cores carried the estate and every reading reached the centre in about a tenth of a second.

Experiment 2 — one site misbehaves

rotterdam-01 was set to publish at 200× normal rate. This ran twice and the two runs disagree, which is the most useful thing in this write-up.

First run: every site sharing one producer client

BaselineFlooding
Total rate4,797/s14,342/s
Errors00
Publish p5058 ms257 ms
End-to-end, the 99 innocent sites p9598 ms330 ms

Read on its own that says a shared cluster does not isolate its tenants, and one bad site costs everyone 3.4×.

It is wrong, and the fault was mine. The simulator published all hundred sites through a single Kafka client, so the other ninety-nine sites' messages were queued behind the flood inside my own producer, before the broker was involved at all. I had measured my gateway and written the broker's name on it.

Second run: the flooding site on its own client

BaselineFlooding, separate clients
Total rate4,797/s14,447/s (4,797 + 9,650)
Errors00
End-to-end, the 99 innocent sites p9598 ms98 ms
End-to-end, houston-01 p9597 ms94 ms
End-to-end, the flooding site p95230 ms

END-TO-END LAG OF THE INNOCENT SITES, p95

98
97
baseline, no flood
330
321
flood, sites sharing a client
98
94
flood, sites on own clients

Solid bar is the ninety-nine well-behaved sites together; the faded bar beside it is one named site, houston-01. Milliseconds.

The middle group is the answer a careless test gives. The right-hand group is what is actually true.

Identical. The ninety-nine well-behaved sites did not notice a neighbour publishing two hundred times its normal volume. Only the offending site slowed down, which is precisely the behaviour you would design for.

WHY THE FIRST ANSWER WAS WRONG

ALL SITES SHARE ONE GATEWAY

innocent sites queue behind the flood, before the broker is involved

p95 → 330 ms
EACH SITE HAS ITS OWN CONNECTION

the flood fills only its own path

p95 → 98 ms, unchanged
The queue forms wherever the sites share something. Give each site its own connection and the broker never sees a problem to solve.

Isolation follows the connection, not the cluster. This is the finding worth carrying into a real design. A hundred sites each holding their own connection are isolated from one another by the broker. A hundred sites funnelled through one shared gateway process are not, and no broker setting can fix it, because the queueing happens upstream of the broker.

Worth checking your own architecture drawing for a single box that every site publishes through — one Node-RED, one integration server, one edge concentrator. If it is there, your sites share a fate regardless of what the cluster is doing.

WHAT ONE BROKER DID

100
SITES ON ONE 2-CORE BROKER
98 ms
SITE TO CENTRE, p95
0
MESSAGES LOST, EVERY RUN
14,447/s
SUSTAINED WITH A FLOOD RUNNING
Capacity was never the interesting question at this size. The architecture around the broker was.

Experiment 3 — one topic per site, or one partitioned topic

One partitioned topicOne topic per site
Rate4,797/s4,797/s
Publish p5058 ms55 ms
Publish p99119 ms337 ms
Topics on the cluster1100
A new site appears to a central consumerimmediatelynot at all

ONE TOPIC PARTITIONED, OR ONE TOPIC PER SITE

ONE PARTITIONED TOPICONE TOPIC PER SITE
throughput4,797/s4,797/s
publish p99119 ms337 ms
topics to manage1100
a new site appearsimmediatelynot until refresh
retention per sitenoyes
access rules per sitenoyes
Only two rows favour a topic per site, and both are governance rather than engineering. If a contract demands them, pay the cost knowingly.

Throughput identical; tail latency 2.8× worse with a topic per site, because tracking a hundred topics is metadata work carried alongside the data.

But the latency is not the problem.

THE FAILURE THAT COST ME AN AFTERNOON

what the consumer subscribed to, at start-upsitesitesitesitesiteCENTRALCONSUMERsite added later0 messages
With a topic per site, the central consumer received nothing at all from sites created after it subscribed. With one partitioned topic, a new site simply starts appearing.

The central consumer was subscribed by pattern — ^plant.site..* — and received zero messages across the entire run. The per-site topics were created after it subscribed, and it never went back to look.

On a real rollout that is the nastiest kind of failure. A site is commissioned, publishes correctly, its data genuinely lands in the cluster, and the central historian does not know it exists. No error, no alarm, no gap in any existing trend. Just a site that never appears, until somebody restarts the consumer.

Per-site topics still earn their place where per-site retention, access control or deletion is a contractual requirement. Pay the cost knowingly, and put "does the centre know about the new site" on the commissioning checklist.

Harness bugs, recorded because both lied

1. The shared producer. Turned "the broker isolates sites perfectly" into "one site degrades everyone 3.4×". The measurement was real and the instrument was the thing being measured. The only reason I checked was that 3.4× looked too tidy for a system that had otherwise refused to break.

2. push(...array) blows the call stack. The consumer merged per-site latency arrays with spread. Under a flood, one site's array reached about 1.8 million elements, and spreading that as function arguments crashed the process with Maximum call stack size exceeded. It failed only on flood runs, so it looked like a load limit rather than a bug in my own code. Fixed with a loop and .slice().

Verdict

At a hundred sites, capacity is not the question. One modest broker carried the estate with enough headroom to absorb a site behaving two hundred times worse than it should, and never lost a message in any run.

Give every site its own connection. Partition one topic by site rather than creating a topic per site, unless governance requires otherwise.

And distrust a result that makes shared infrastructure look bad until you have checked what else in the path is shared.

The argument, without the measurements, is in the article.

Newsletter

New essays, by email.

SCADA, cloud, AI, and the plant floor — a short email when something new is published. No noise, unsubscribe anytime.