topics / async-messaging
Message Queue
A generic event-driven pipeline, not one broker's API — a bounded backlog, competing or fanned-out consumers, and what actually happens when one crashes mid-job or the producer outruns them. Switch delivery modes, kill a consumer, and watch redelivery, retries and the dead-letter queue do their jobs.
What a message queue actually buys you
A message queue sits between something that produces work (an API handler, a cron job, another service) and something that does the work, and it decouples the two in time. The producer doesn't call the consumer directly and wait for a response — it drops a message and moves on, trusting the broker to hold it until a consumer is free. That single property is where every benefit comes from: the producer never blocks on a slow consumer, a consumer can crash and restart without the producer noticing, and you can scale the two sides independently instead of matching one API call to one unit of downstream work.
“Event-driven architecture” is the same idea at a larger scale — services publish events (“order placed,” “payment failed”) instead of calling each other directly, and whoever cares about an event subscribes to it. A message queue is the mechanism that makes that decoupling actually work under load, which is what this simulation focuses on rather than any one product's API.
Two generic delivery shapes
Almost every broker you'll encounter — SQS, RabbitMQ, Kafka, Redis Streams — is really offering some mix of two underlying shapes. The simulator lets you switch between them directly rather than picking a product, because the difference is the thing worth understanding.
Work queue (competing consumers)
Fan-out (pub/sub)
Try it: switch to fan-out, add a couple of consumers, and publish a burst. Watch the same message travel to every consumer at once, and notice each consumer's own backlog count on its card — a slow consumer accumulates a backlog that has nothing to do with how the others are doing, because they aren't sharing one queue anymore.
Backpressure: what happens when the producer wins
A backlog has a capacity for a reason — an unbounded queue just moves the memory problem from the consumer to the broker. Once it's full, something has to give, and the simulator's two backpressure policies are the two real answers:
Drop rejects new messages outright once the backlog is full — simple, and it protects the broker, but it silently loses work unless the producer is built to retry or the loss is acceptable (metrics, best-effort logs). Pause the producerinstead makes the publish call itself block until there's room — nothing is lost, but now a slow consumer can stall the producer, which can cascade upstream if that producer is, say, a request handler with its own caller waiting.
Turn the processing time up, start auto-publish at a high rate, and watch the backlog fill toward capacity under either policy — then use Add consumer to scale the work side and watch it drain. That scaling response is the entire pitch for decoupling producer from consumer in the first place: the fix for a growing backlog is more workers, not a faster producer.
Acknowledgment, visibility timeout, and redelivery
A consumer doesn't just read a message — it has to tell the broker it actually finished the work (an ack) before the broker considers the message done. That handoff is what makes a consumer crash survivable instead of catastrophic: if a worker dies mid-job, it never acks, and the broker eventually notices and hands the message to someone else.
“Eventually notices” is the visibility timeout: the moment a message is handed to a consumer, the broker hides it from everyone else for that long, betting that the consumer will finish and ack before it expires. Hit Killon a busy consumer and watch its message go quiet — the broker has no idea anything went wrong yet — until the timeout expires and it's redelivered to another consumer. Set the timeout too short and you get duplicate processing (the original consumer was still working, just slow); set it too long and a genuine crash sits unnoticed for a while. There's no value that's simply correct — it's a bet on how long real work should ever take.
Notice, too, that the killed consumer itself restarts on its own a few seconds later, empty and ready for new work — that recovery is independent of the message's own timeout. A supervisor (Kubernetes, ECS, a process manager) restarting a dead worker and a broker redelivering an unacked message are two separate mechanisms solving two separate problems, running on their own clocks.
Retries and the dead-letter queue
Redelivery assumes the failure was transient — a dead process, a network blip — and that trying again will work. Sometimes it won't: a message with a bug in it, or pointed at a resource that's permanently gone, will fail the same way every single time, and blind retries just burn consumer capacity forever without ever finishing. That's what max retries and the dead-letter queue are for — after a message fails (or crashes its consumer) that many times, it stops being retried and moves to a separate queue for a human, or a different process, to look at later.
The simulator's job failure ratemodels the other failure path: a consumer that runs to completion but the work itself throws (a bad record, a downstream 500) — an explicit nack, no crash and no visibility timeout involved, just an immediate retry-or-dead-letter decision. Turn it up and watch messages cycle through the backlog a few times, going amber each retry, before landing in the dead-letter queue. In a real system, that queue is the thing you monitor — a dead-letter count that's climbing means something is wrong in a way retries can't fix.
| Work queue | Fan-out (pub/sub) | |
|---|---|---|
| Who gets each message | Exactly one consumer | Every consumer, independently |
| Backlog | One, shared | One per consumer |
| Adding a consumer | Spreads the same work further | Adds a new, independent subscriber |
| A slow consumer | Shrinks everyone's throughput — it's one shared line | Only backs up its own copy of the stream |
| Typical use | Job queues: emails, image resizing, payments | Events: order placed, user signed up, price changed |