Work Distribution: Why Only One Competing Consumer Should Get Each Job
You have 200 thumbnail-generation jobs in a queue and eight workers pulling from it. The requirement is that each image is processed once — if all eight workers were subscribers to every job, you would render 200 images eight times and pay 8x compute for the privilege. Queues solve this by construction: a message published to a queue is delivered to exactly one of its competing consumers. The broker tracks the in-flight message and holds it from others until the consumer acks, fails, or times out.
The semantics that make this safe, concretely:
publish → QUEUE (one logical list)
├─ consumer A ─┐
├─ consumer B ├─ COMPETING: broker delivers each message
├─ consumer C │ to ONE; ack removes it; crash/timeout
└─ ... ┘ returns it for redelivery
⇒ scaling workers scales THROUGHPUT, not duplicate delivery
Three properties define the point-to-point contract:
| Property | Queue behavior | Why it matters |
|---|---|---|
| Cardinality | one consumer per message | work is not multiplied; workers are interchangeable |
| Visibility/ack | broker holds the message while in flight | crash before ack ⇒ redelivery to a consumer, not all |
| Fan-out | none by design | a second queue (or a fan-out bridge) is required for other teams' needs |
The last row is where teams get burned. A payments team puts payment.settled on a queue because 'a queue is for messages'. The fraud team also wants those events — so they attach a second consumer to the same queue and now every settlement is either consumed by fraud or by the ledger, never both: two subscribers competing for one stream is a routing bug that looks like an outage weeks later (one consumer silently starves the other; the missing data shows up as a reconciliation gap, not an error). The fix is structural: one queue per competing work pool, and independent delivery per interested party.
On which broker carries this: RabbitMQ queues (one message, one consumer, ack-based redelivery), SQS standard/FIFO queues (identical model; FIFO adds per-MessageGroupId ordering and content-based deduplication), and — this trips people up — Kafka partitions behave like queues for messages with the same key but only when a single consumer group is attached. One consumer group per work pool: within the group, partitions are split among members so each record goes to one member; that is your competing-consumers pool. (More in chapter 2.)
Sizing rules of thumb for a work queue: number of concurrent workers should be bounded by downstream capacity (a queue hides bursts; it must not be permission to overrun the database), redelivery timeout must exceed job duration, and work must be idempotent because at-least-once is the default everywhere (Message Queues and Brokers covers the ack mechanics). In interviews, the one-line discriminator to say first: a queue answers 'who should do this work?', a topic answers 'who needs to know this happened?' One follow-up worth being ready for: how many workers should a queue have? Size it from downstream capacity, not from CPU headroom — a queue absorbs bursts, it does not license an unbounded number of concurrent database writers — and remember that adding workers never adds per-message ordering; the ordering key lives in the data, not in the pool.
Fan-Out: One Publish, Five Subscriptions, Each Getting Its Own Copy
order.placed fires once, and five teams care: the warehouse reserves stock, analytics increments revenue, the recommendation model trains, email sends a receipt, and fraud scores the order. Wire these as competing consumers and each event triggers only one of the five — inventory would randomly miss orders while email 'sends receipts' for shipments it never saw. The publish-subscribe pattern exists for exactly this: one publish, one copy delivered to every active subscription.
How each broker realizes fan-out differs, and the differences matter operationally:
| Broker | Unit of subscription | Independent progress | Consumer crash behavior |
|---|---|---|---|
| Kafka | consumer group (group id = subscription) | offsets per group per partition | group resumes at its own committed offset; other groups unaffected |
| RabbitMQ | queue bound to an exchange | each queue has its own backlog | unacked message requeued to that queue only |
| Pub/Sub (GCP) | subscription | ack deadline per subscription | redelivers to that subscription only |
| SNS / EventBridge | subscriber endpoint | fan-out at publish time | retry policy per subscriber with DLQ |
The key insight for Kafka users: a topic is inherently publish-subscribe; the queue semantics emerge from how many consumer groups you attach. orders topic with one group invoice-workers = work queue. The same topic with groups warehouse, analytics, email = five independent streams, each replaying from its own offset. This dual behavior is Kafka's most misunderstood property — and it is why adding a new consumer group can backfill months of retained history for free, while adding a member to an existing group only redistributes partitions.
Now the cost side of fan-out, because interviews should include it:
- Storage multiplies per subscription lag. Retention is on the log, but each slow group builds its own backlog; a group stalled for 6 days on a 7-day retention topic loses records when retention evicts them — silently, with no consumer error.
- Slow subscribers do not block publishers (that is the point), but they do accumulate cost: Kafka with 12 groups means 12x the reads and the slowest group dictates nothing except its own staleness.
- Fan-out depth is a coupling graph. Ten topics × eight subscriptions = 80 edges to reason about when a schema changes (Schema Evolution is where that bill comes due). A topic map drawn on one page is a design artifact worth having.
- Ordering is per partition per group. Two groups may process the same events in different wall-clock orders (each follows its own lag), so cross-team invariants cannot rely on 'everyone saw X before Y'.
RabbitMQ's shape deserves one extra sentence: producers publish to an exchange, and bindings decide which queues receive copies — one exchange with N bound queues is classic fan-out, while a direct exchange with one queue per routing key gives work distribution per key. The exchange-plus-bindings layer is what lets RabbitMQ do both patterns without changing the producer.
Interview formulation: name the cardinality question first — how many interested parties must receive this message? One, and it is a queue (or one consumer group). Many, and it is a topic with one subscription per party. Getting this backwards produces one of two symptoms: duplicate work (everyone processed it) or missing consumers (nobody in particular processed it) — the failure catalog in the final chapter.
Wrong Pattern, Wrong Symptom: How a Miswired Consumer Shows Up Weeks Later
The bug reports look unrelated. Team A reports that invoices are missing for 3% of orders; Team B reports processing the same shipment twice and double-charging freight; Team C reports a consumer that has processed nothing since Tuesday while its dashboard stays green. All three trace to one mistake each: A attached a second consumer to a queue that already had one, B let every worker create its own consumer group on the shared topic, and C subscribed a consumer group that no producer ever publishes under. Messaging errors are diagnosable once you know which symptom each wiring produces.
The failure catalogue:
COMPETING CONSUMERS ON A FAN-OUT NEED → MISSING CONSUMER
payments queue: ledger and fraud both pull → each settlement goes to ONE
symptom: partial data, no errors, reconciliation gaps found downstream
EACH WORKER IN ITS OWN GROUP → DUPLICATE WORK
order topic, 5 workers, 5 group ids → every order handled by all 5
symptom: 5x processing cost, double-reserved stock, racing updates
ORPHAN SUBSCRIPTION → SILENT STALL
group id 'analytics-v2' but producers publish only to 'events.v1'
symptom: consumer healthy, lag 0, no data — because there is no data
PER-SUBSCRIBER FAILURE → CROSS-TEAM CONTAGION
one webhook subscriber down on a shared topic → its retries absorb broker capacity,
its DLQ fills, and unrelated groups inherit latency
A decision procedure you can run in a design review, taking one minute:
- Who are the recipients — one or many? If you must enumerate the interested parties today and expect a sixth next quarter, it is fan-out (topic/exchange with one subscription each). If recipients are interchangeable workers of one service, it is a queue (or one consumer group).
- What happens if two parties get it? Duplication tolerated (idempotent projection)? Then a topic with multiple groups is safe by definition — each group sees it exactly once (per its own offsets). Not tolerated (money movement)? Then ownership must be exclusive: a queue, or a single-writer consumer group with a lock.
- What happens if a party misses it? If missing is unacceptable, the subscription needs its own retention-based replay and a lag alarm — a bound exchange with no queue (RabbitMQ transient bindings) delivers only to currently attached consumers, which is the classic 'we weren't running at publish time' loss.
- Is the delivery contract written down? Per group: at-most-once, at-least-once, ordering key, retention vs max lag. Undocumented contracts are how a schema change later breaks a consumer you never knew existed.
Note the asymmetry in those answers: a queue fails closed (if no worker is up, messages accumulate; if a worker is up, someone owns each message) while a fire-and-forget subscription fails open (if the subscriber is down at publish time with transient delivery, the message is simply gone). That is why the same product may use a topic to announce payment.settled and a queue to dispatch settle-payment work — announce once to many, assign work to one.
In an interview you can demonstrate this whole model in three sentences: state the cardinality question, name the two symptoms (duplicate work vs missed consumers), and point out that Kafka supports both over one topic depending on consumer groups — then say which one your feature uses and why. From here the roadmap generalizes the producer/consumer side into Event-Driven Architecture Patterns.
Quiz
1. A payments queue has two consumer groups attached, 'ledger' and 'fraud'. What happens to a newly published payment?
2. Three services must each receive every 'order.placed' event, and each processes at its own pace. Which wiring is correct?
3. Your analytics consumer group has processed zero messages for two days while reporting healthy and lag 0. What is the most likely cause?
4. An order-processing work pool was accidentally built with each worker creating its own Kafka consumer group id instead of sharing one. What happens?
Flashcards
Question
Point-to-point queue vs publish-subscribe topic: the one question that decides
Click to reveal answer
Answer
Ask 'how many interested parties must receive this message?'. One (interchangeable workers doing the job) → queue/competing consumers: exactly one consumer gets each message. Many (independent teams/services each needing a copy) → topic with one subscription per party: every subscriber gets its own copy with independent progress.
Question
How does Kafka express both patterns over one topic?
Click to reveal answer
Answer
Consumer groups are the subscription unit. One group with several members = competing consumers (partitions split, each record to one member) = queue semantics. Several groups = fan-out (each group reads all records from its own offsets) = pub/sub semantics. Offsets are per group per partition, so subscriptions never interfere.
Question
What two classic symptoms come from choosing the wrong pattern?
Click to reveal answer
Answer
Competing consumers attached where subscribers were needed → missed consumers: each message handled by only one party, showing up as partial data and reconciliation gaps with no errors. Subscribers attached where competitors were needed → scattered/duplicate work: overlapping or unfilled processing depending on membership, with orders randomly owned by nobody in particular.
Question
Why does RabbitMQ need an exchange and bindings to support both patterns?
Click to reveal answer
Answer
Producers publish to an exchange; bindings decide which queues receive copies. One queue bound = work distribution (competing consumers on that queue). N queues bound to one exchange = N independent subscriptions with their own backlogs and acks. Without bindings there is no fan-out — and transient bindings fail open, dropping messages published while no subscriber is attached.
Question
What is the risk of a slow consumer group on a Kafka topic with 7-day retention?
Click to reveal answer
Answer
Retention is independent of consumption, so if the group's lag exceeds 7 days its unread records are deleted underneath it — data disappears without a consumer error, surfacing only as a gap in its output. Fix: alarm on lag age (not just offset count), extend retention for critical topics, or reprocess from a backup/archive.
Revision Notes
Key Takeaways
- 1. The pattern question is cardinality: one owner of the work versus many interested parties — answer it before choosing a broker feature
- 2. Kafka implements both patterns through consumer groups, which is why 'is this topic a queue?' is unanswerable without naming the groups
- 3. Wrong-pattern bugs fail silently: missing consumers look like reconciliation gaps and miswired work pools look like randomly owned orders
- 4. Fan-out subscriptions each own their lag, and lag that exceeds retention deletes data without a consumer error
- 5. Queues fail closed (backlog accumulates) while transient subscriptions fail open (messages gone at publish time) — pick per your loss tolerance
Interview Tips
- • "Queue or topic for this feature?" — answer with the recipient question, then the double-processing test (if two parties must not both act, ownership must be exclusive) and the miss test (if missing is unacceptable, durable subscription plus replay)
- • "How do you scale order processing horizontally?" — competing consumers in one group/partition assignment, idempotent handlers, and lag-based autoscaling; add that adding members rebalances rather than duplicates
- • "Why does my consumer show lag 0 but no data?" — orphan subscription: group or topic mismatch; show you check producer publish targets before suspecting the broker
- • "Explain Kafka's dual behavior" — one group = queue semantics, several groups = pub/sub, offsets per group per partition, and a new group can replay retained history — a capability interviewers like to probe
Cheat Sheet
Pub/Sub vs Point-to-Point
The deciding question: who must receive it?
ONE (work) → QUEUE / competing consumers: broker delivers each message to exactly
one consumer; ack removes; crash ⇒ redeliver to A consumer
⇒ scaling workers scales throughput, not duplication
MANY (notify) → TOPIC / subscriptions: every subscription gets its own copy with
independent offsets/backlog and independent lag
| Broker | Subscription unit | Per-subscriber state |
|---|---|---|
| Kafka | consumer group | offsets per group per partition (one group = queue, many = fan-out) |
| RabbitMQ | queue bound to exchange | per-queue backlog + acks |
| GCP Pub/Sub | subscription | per-subscription ack deadline |
| SNS/EventBridge | subscriber endpoint | publish-time fan-out + per-subscriber retries/DLQ |
Symptom → diagnosis:
missing downstream data (no errors) → competitors where subscribers were needed
stalled/duplicated work → subscribers where competitors were needed
green consumer, lag 0, no data → orphan subscription (topic/group mismatch)
one subscription's outage hurting all → shared broker capacity + unbounded retries
Decision procedure: (1) enumerate recipients today and next quarter → many = topic · (2) double-processing unacceptable → exclusive ownership: queue or single group with locking · (3) missing unacceptable → durable subscription + replay + lag alarm (transient bindings fail open) · (4) write down the delivery contract per subscriber: ack mode, ordering key, retention vs max lag.
Kafka gotchas: new group backfills retained history for free · lag beyond retention = silent data loss · ordering per partition per group · one hot key = one partition.