The Runtime Theory
SystemInternalsarchitecture

Fanout: One Event, N Consumers, and the Isolation Between Queues

A step-by-step walk from the single published event through topic routing to per-consumer queues with independent offsets and failure isolation.

The Runtime Theory Team3 min read06 steps

trace spine

  1. 01 event is published once
  2. 02 broker routes to topic
  3. 03 each consumer group reads
  4. 04 offsets advance independently
  5. 05 a slow consumer lags
  6. 06 failure isolation holds

Fanout is the "tell everyone" primitive: one event, many consumers, each with its own independent read position. The architecture that makes this safe is a single write point with per-consumer state. Trace one order.created event through the system.

trace stepSystem
The order service publishes order.created to the orders topic — one record, one partition append, one acknowledgment. Cost: one broker write (~1–5ms with acks=all in Kafka). The producer does not know or care how many consumers will read it. That asymmetry — one write, N reads — is the entire point of fanout.
trace stepSystem
The broker keeps the event on the topic's partition(s). Consumers subscribe: the email service, the analytics pipeline, the inventory service, the search indexer. In Kafka terms each is a consumer group; in RabbitMQ terms each binds a queue to the exchange. Routing is declarative and happens at the broker, not in the producer.
trace stepSystem
All four consumers read the same record independently — the analytics group reads offset 41 of partition 0 at the same moment the email group reads offset 41 of partition 0. They share the bytes on disk but never share progress. This is the key difference from a work queue: a queue splits messages among workers; a topic broadcasts them to groups.
trace stepSystem
Each group commits its own offset at its own pace. The email group commits after a fast 5ms process; the analytics group, batching for efficiency, commits every 10 seconds. Neither's position affects the other's. A group that rebalances (a consumer joins or leaves) resets to its own last committed offset and replays only its own unconsumed tail.
trace stepSystem
The search indexer's downstream API stalls; its group stops committing. Its lag grows: 100, 10,000, 100,000 records. Nothing else feels it — email and analytics keep committing, the broker keeps the lagging group's records for retention (Kafka default: 7 days). The slow consumer eventually recovers and catches up from its own offset, reprocessing only its own backlog.
trace stepSystem
This is the payoff: if the analytics consumer dies permanently, the system degrades one consumer, not the pipeline. The event was never deleted on consumption (that's the queue model); it remains on the topic until retention, so a repaired or replaced consumer resumes exactly where its group stopped. The only shared resources are broker disk and the topic's retention budget.
text
order.created ──▶ topic "orders" (partitioned, retained)
                    ├── group: email       → offset per partition, fast
                    ├── group: analytics   → offset per partition, batched
                    ├── group: inventory   → offset per partition
                    └── group: search      → offset per partition, lags, recovers

The ledger for a 10-consumer fanout of a 1,000 msg/s event stream: one producer write at ~1ms, ten consumer reads at ~1ms each, ten offset commits per interval, and broker disk for the 7-day retention window. The mechanism's guarantee — every consumer sees every event, at its own pace, without ever blocking the others — is what event-driven architectures are built on, and the isolation is the reason one broken consumer is an incident for one team, not for the platform.