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.
producer10 msg/senqueueheadtailWORKERS · 1worker-15.0 msg/sSYSTEM THROUGHPUT5.0 / 10.0 msg/sdepth = 0
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 lock
2def handOutNext(workerId):
3 with head_lock:
4 if headIndex >= len(events):
5 return None # queue empty
6 cell = events[headIndex]
7 cell.takenBy = workerId
8 headIndex += 1 # advance past this cell
9 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 continue
7 process(cell) # ~1 / per_worker_rate seconds
8 # 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/s
2def systemThroughput(N, producerRate, perWorkerRate):
3 drainCapacity = N * perWorkerRate
4 return min(producerRate, drainCapacity)
5
6# below the knee: N * perWorkerRate < producerRate
7# -> drain-bound; depth grows; add workers to keep up
8# above the knee: N * perWorkerRate >= producerRate
9# -> producer-bound; depth flat; extra workers idle