Build a Message Queue (RabbitMQ / SQS) (14 scenes)
Scene 04 · Competing consumers — N workers, one head
Point N workers at the same head; the broker hands each cell to whichever worker is ready. Throughput scales linearly until producer rate caps it.
Previously
We had one producer and one consumer on a tidy strip. Real workloads need throughput, so we point N workers at the same head and let the broker hand each message to whichever worker is ready.
Scene 04
Competing consumers — N workers, one head
Diagram
One queue strip in the middle: producer on the upper-right enqueues colored cells at a fixed rate; the head is on the left. On the right, a stack of N workers each compete for the next available cell — this is the **competing consumers** pattern. The broker hands each cell to whichever worker is ready, and a cell goes to EXACTLY ONE worker (colors never duplicate across the stack). The throughput meter below sums per-worker throughput; the depth badge shows how many cells are waiting.
One worker can't keep up with the producer — watch the strip grow. Each new worker shares the same head, and a cell goes to EXACTLY ONE worker (no duplicate colors across the stack). Three workers is enough to drain the queue at this rate.
Implementation
Broker.handOutNext(workers)
atomic pop at the head — one cell goes to exactly one worker
1# called by each ready worker; runs under a head lock2def handOutNext(workerId):3 with head_lock:4 if headIndex >= len(events):5 return None # queue empty6 cell = events[headIndex]7 cell.takenBy = workerId8 headIndex += 1 # advance past this cell9 return cell
Worker.loop()
pull from the shared head, process, repeat
1def run(workerId):2 while True:3 cell = broker.handOutNext(workerId)4 if cell is None:5 sleep(poll_backoff_ms)6 continue7 process(cell) # ~1 / per_worker_rate seconds8 # ack is implicit here — see next scene for the split
System.throughput
drain capacity capped by whoever is slower
1# steady-state system throughput, in msg/s2def systemThroughput(N, producerRate, perWorkerRate):3 drainCapacity = N * perWorkerRate4 return min(producerRate, drainCapacity)56# below the knee: N * perWorkerRate < producerRate7# -> drain-bound; depth grows; add workers to keep up8# above the knee: N * perWorkerRate >= producerRate9# -> producer-bound; depth flat; extra workers idle