0% completed
Message Queue
On This Page
- The Incident
- The Naive Fix, and Why It Fails
- The Pattern
- Walkthrough with Numbers
- Trade-offs: What You Pay
- When NOT to Use It
- Pattern Face-Off: Queue vs Pub/Sub
- In the Wild
- The Interview Lens
- Check Yourself
- Composes With
- TL;DR Card
1. The Incident
Your store runs a limited drop: 10,000 signed editions go on sale at 10:00 AM. On a normal day you get 200 orders per second. At 10:00:01, you're getting 5,000 orders per second, and it stays that way for two minutes.
Your order pipeline is synchronous, one long phone call: the API takes the order, reserves inventory, writes the order to the database, schedules fulfillment, and only then tells the buyer "success." That whole pipeline can handle 400 orders per second. It's been plenty for years.
Now do the math with me. 5,000 orders arrive each second. 400 leave each second. The other 4,600 wait, time out, or die. For two minutes, 92% of the people trying to give you money see an error page. Frustrated buyers hit refresh, which adds even more traffic. By lunch, the press is writing about your "crash."
Here's the part that stings in the postmortem. Count the total work: 600,000 orders arrived in two minutes. Your pipeline can process 600,000 orders in well under a day. And nobody expects their package within two minutes anyway. You had plenty of capacity. What the buyers actually needed instantly was just the confirmation: "got it, you're in." The work of fulfilling the order was never urgent.
Your architecture glued those two things together. Because the confirmation and the work happened in the same phone call, the work inherited the confirmation's deadline. That's the real bug.
2. The Naive Fix, and Why It Fails
"Scale up for the peak." Add servers until the pipeline handles 5,000 per second. That's 12 times your normal fleet, paid for all year, used for two minutes. And it doesn't even work: the database under the pipeline has its own limit, so the flood just crashes one level deeper. You moved the cliff. You didn't remove it.
"Buffer the orders in memory." Have the API hold incoming orders in a list and work through them. You've just invented a queue, minus everything that makes queues safe. If the process crashes, the list vanishes, and those were paid orders. Other API servers can't see the list. And nothing in your monitoring shows how long the list is until the server runs out of memory.
"Reject the extra traffic." Return "too busy, try later" to everything above 400 per second. For some traffic, that's a fair answer. But this is the biggest sales event of your quarter. You'd be deliberately turning away 9 out of 10 customers to protect a pipeline that could easily do the work an hour later.
All three fixes miss the diagnosis. This isn't a capacity problem. It's a coupling problem: the rate orders arrive and the rate you process them have no business being forced to match, second by second.
3. The Pattern
A message queue is a durable to-do list that sits between the producer and the consumer. The producer adds a task and moves on immediately. The consumer works through tasks at its own steady pace. The queue keeps every task safe until a consumer confirms it's done.
That's the to-do list from the module intro. You don't stand next to a colleague while they do the task. You write it down somewhere safe, they work through the list, and the list itself remembers what's pending.
Four mechanics make the promise real. Each one answers a "but what if..." question:
- The queue saves the message before saying "accepted." The broker (the software running the queue) writes the message to disk first, then confirms to the producer. From that moment, a worker crash, a deploy, or an hour of downtime loses nothing. The work just waits.
- Workers confirm after finishing, not before. A worker takes a message, does the job, and only then sends an acknowledgment ("ack"). What if the worker crashes halfway through? The broker waits a set time (often called the visibility timeout), hears no ack, and hands the message to another worker. Notice what this means: if a worker finished the job but crashed just before acking, the message gets handed out again, and the job runs twice. This is where at-least-once delivery comes from. The price of never losing work is sometimes doing it twice. That's why every consumer needs idempotency: the skill of making repeated work harmless. That lesson covers it fully; for now, remember Rule 2 from the intro: duplicates are normal, plan for them.
- Many workers share one queue. Each message goes to exactly one of them. Need more throughput? Add workers. No coordination code needed. This setup is called competing consumers.
- Queue depth is your most honest metric. Depth means "how many tasks are waiting." It's simply arrivals minus completions, piling up over time. Depth rising during a spike is the pattern working. Depth rising for a week straight is a warning we'll get to in Q2.
Put together, the queue separates three things that the synchronous design had glued into one:
- Rate: producers can burst to 5,000 per second while workers calmly do 400.
- Time: workers can be down, deploying, or not built yet. The work waits.
- Failure: a worker crash means a redelivery, not lost data, and the producer never even notices.
4. Walkthrough with Numbers
Replay the flash sale, this time with a queue between the API and the pipeline.
- 10:00 to 10:02. 5,000 orders arrive per second. The API validates each one, adds it to the queue, and replies "Order confirmed!" in about 50 ms. Buyers experience the smoothest flash sale of their lives. Meanwhile the queue is filling: 5,000 come in and 400 go out each second, so it grows by 4,600 per second. Over 120 seconds, that's a backlog of about 552,000 messages. The queue doesn't care. Disk is cheap.
- 10:02 to 10:25. Workers keep grinding at 400 per second. To clear 552,000 messages at 400 per second takes 552,000 ÷ 400 = 1,380 seconds, about 23 minutes. During that window, an order's fulfillment might start 20 minutes after purchase instead of instantly. Nobody notices, because the confirmation email says "shipping today," and that's true. Important: someone chose this trade with the business beforehand. That's what makes it a design instead of an accident.
- 10:07, the failure drill. A worker dies mid-message. The visibility timeout expires. Another worker picks the message up and finishes it. Its idempotency check makes sure the half-done work doesn't apply twice. Zero orders lost. Nobody got paged.
- The bill. One queue service, costing pennies per million messages, replaced the "12× the fleet" plan.
One sentence to keep: the queue didn't add capacity. It gave your pipeline permission to be slow, which the business could always afford; the synchronous design just couldn't use it.
5. Trade-offs: What You Pay
You gain:
- Spikes become backlog instead of errors, limited by disk instead of by your slowest service.
- A worker being down becomes a growing to-do list, not an outage. Deploys stop being scary.
- Producers and workers scale separately, and queue depth is a ready-made signal for auto-scaling.
- Redelivery gives you retry behavior for free; the broker does it for you.
You pay:
- Latency, on purpose. Work now finishes seconds to minutes later. Anything the user actually waits for does not belong on a queue.
- Duplicates. At-least-once delivery means workers must be idempotent. This is a requirement, not a nice-to-have.
- Ordering gets messy. Twenty workers process in parallel, so messages no longer finish in the order they arrived. You can get order back per key (all messages for one customer stay in sequence) at some throughput cost. Q3 shows the bug you get for ignoring this.
- A gap between "accepted" and "done." The order is confirmed but not yet fulfilled. Product has to decide what users see during that gap.
- More moving parts. A broker to run, depth and message age to monitor, a place for failed messages (the dead letter queue lesson), and debugging that now spans systems instead of one stack trace.
6. When NOT to Use It
- The caller needs the answer. Login checks, price lookups, search results: the user is on the phone, waiting. That's request-response. You cannot email someone their search results.
- Tight end-to-end deadlines. If the whole job must finish in 200 ms, a broker hop plus a waiting worker adds risk for no benefit.
- Small, simple systems. A service doing 10 requests per second doesn't need a broker between two of its own functions. It needs a function call. Queues earn their complexity at scale, at spikes, or at failure boundaries.
- As a database. A queue is a conveyor belt, not a shelf. If you need to query it, update items, or keep them forever, you want a real data store.
And the sneakiest misuse: using the queue to hide a capacity problem. If depth trends upward for days, you're not absorbing a spike anymore. Arrivals permanently exceed capacity, and the queue is just concealing it until the disk fills. As the bulkhead lesson will put it: you don't need a better buffer, you need a bigger ship.
7. Pattern Face-Off: Queue vs Pub/Sub
This module's definitive confusion, and a favorite interview probe. Both use a broker. Both are asynchronous. They answer different questions.
| Message Queue | Pub/Sub | |
|---|---|---|
| A message is | A job: work to do once | A fact: something that happened |
| Delivered to | Exactly one of the workers | Every subscriber, separately |
| Adding a consumer | More hands for the same work | A new use of the same news |
| The question it answers | "Who does this task?" | "Who cares that this happened?" |
"Resize this image" is a job. One worker should do it, once: queue. "An order was placed" is a fact. Fulfillment, email, analytics, and fraud all care, each for their own reasons: pub/sub. And the two compose: announce the fact once, and let each interested team feed its own work queue from the announcement. That's exactly what AWS's SNS-into-SQS setup does, and what Kafka's consumer groups give you built in. If you can explain that last sentence, this face-off is yours.
8. In the Wild
- AWS SQS is the pattern in its purest form: visibility timeouts, dead-letter support, near-infinite depth, pennies per million messages.
- RabbitMQ is the classic self-hosted broker, with rich routing options on top of the same ack-and-redeliver core.
- Kafka is technically a replicated log rather than a queue, but its consumer groups behave like one, and its ability to replay old messages feeds event-driven architecture and the stream processing patterns later in the course.
- Celery, Sidekiq, BullMQ: the same pattern packaged as an application library. This is where most engineers meet it first.
For AI engineers: GPU work is the most queue-shaped workload in modern infrastructure: expensive, bursty, and usually not urgent. Embedding pipelines, batch inference, fine-tuning jobs, and evaluation runs are all fed by queues, and queue depth (or better, the age of the oldest message) is the standard signal for GPU auto-scaling. A RAG ingestion pipeline is this pattern with the dead letter queue as its safety net. And the batch APIs from Anthropic and OpenAI are simply this pattern sold as a product, at half price, because you granted the permission to be slow.
9. The Interview Lens
How it's asked: "How do you handle a 10× traffic spike?" and "Design an order pipeline" are both queue questions in disguise. The senior version comes as a review of your own diagram: the interviewer points at a synchronous arrow and asks, "Does the user need to wait for this?" Every arrow where the honest answer is "no" is a queue you haven't drawn yet.
The 30-second answer: "I put a durable queue between the API and the pipeline. The API validates, enqueues, and confirms in milliseconds, and workers process at their sustainable rate, so a 5,000-per-second spike becomes backlog instead of errors. The broker saves each message before confirming and redelivers if a worker dies, which means at-least-once delivery, so my consumers are idempotent and repeatedly-failing messages go to a monitored dead letter queue. Queue depth drives my worker autoscaling and alerting. The trade is latency: this only works because the business agreed fulfillment can take minutes, and anything the user actually waits for stays synchronous."
Likely follow-ups:
- "A worker crashes halfway through a message. Walk me through it." The visibility timeout expires, the broker redelivers to another worker, the idempotency check prevents double-applying, and if the message fails several times in a row it goes to the dead letter queue and triggers an alert. (This follow-up chains straight into idempotency and the dead letter queue; answering it fluently is exactly what those lessons build.)
- "Orders for the same customer must process in order. Now what?" With competing consumers, global order is gone. So you partition by customer ID (Kafka partition keys, SQS FIFO message groups): strict order within each customer, parallel processing across customers. The honest cost: one very busy customer is limited to a single lane's speed.
Red flag that fails candidates: putting a queue on a path the user is waiting on ("the search request goes into Kafka..."), or presenting a queue with no answer for duplicates. The first says you memorized the shape but not the reason. The second says a duplicate delivery hasn't burned you yet.
10. Check Yourself
Q1 (recall). A queue separates producers from consumers in three ways. Name them, and explain step by step where at-least-once delivery comes from.
Q2 (trade-off). Queue depth has grown steadily for six days. A teammate proposes raising the queue's storage limit. What is the depth actually telling you, and why is the proposal a trap?
Q3 (scenario). Charges and refunds for the same customer flow through one queue with 20 workers. Finance reports that a customer's refund was processed before the charge it refunds, leaving their balance negative. Explain how that happened, and fix it without giving up parallel processing.
<details> <summary>Answers</summary>A1. Rate (producers burst while workers process steadily), time (workers can be down; the work waits safely), and failure (a worker crash becomes a redelivery, not lost data). At-least-once delivery, step by step: workers ack only after finishing; the broker redelivers anything not acked in time; the broker cannot tell "crashed before finishing" apart from "finished but crashed before acking"; so it must redeliver in both cases, and the second case produces a duplicate.
A2. Six days of steady growth means arrivals have permanently outpaced processing. This is not a spike being absorbed; it's a capacity shortfall being hidden, and the cost is compounding: the oldest message is now six days late. Raising the storage limit buys a bigger rug to sweep the problem under. The real options: add worker capacity (or fix the workers' bottleneck), reduce or reprioritize what flows in, or change what work goes through this path at all. And alert on the age of the oldest message, not just depth. Age is what users actually feel.
A3. Twenty parallel workers destroyed the ordering: the charge and the refund were picked up by different workers, and the refund's worker happened to finish first. Fix it with key-based ordering: partition the queue by customer ID (a Kafka partition key or an SQS FIFO message group), so each customer's messages process strictly in sequence, one lane per customer, while different customers still run in parallel. Two costs to say out loud: a single very active customer maxes out at one lane's throughput, and a stuck message now blocks that customer's lane (note the dead letter queue lesson's ordering caveat: for strictly ordered streams, dead-lettering trades correctness, so this lane may need pause-and-fix instead).
</details>11. Composes With
- Idempotency: at-least-once delivery makes every consumer a retry target; this is the queue's non-negotiable prerequisite.
- Dead Letter Queue: where messages go after redelivery gives up; the queue's safety net, made real by its alarm.
- Retry with Exponential Backoff: redelivery is retrying, done by the broker; tune attempt counts and delays with that lesson's logic.
- Publish-Subscribe: the sibling for facts rather than jobs; often composed as one announcement fanning out into per-team queues.
- Auto-Scaling: queue depth and backlog age are the cleanest scaling signals in distributed systems.
12. TL;DR Card
| <div style="width:85 px">Problem </div> | A synchronous pipeline forces work to happen at the speed orders arrive, so a spike becomes an outage even when total capacity is plentiful. |
| Mechanism | A durable to-do list between producer and consumer: save before confirming, deliver each message to one worker, redeliver on crashes, drain at the workers' own pace. |
| Costs | Latency by design, duplicates (so consumers must be idempotent), lost global ordering, an "accepted but not done" gap for product to own, and a broker to operate. |
| Skip it when | The caller needs the answer now, the deadline is tight end-to-end, the scale doesn't justify a broker, or you'd be hiding a permanent capacity shortfall. |
| 30-second answer | Enqueue the work, confirm instantly, let workers drain at their own pace: spikes become backlog, crashes become redeliveries, depth drives scaling. The price is latency, duplicates, and ordering, so consumers stay idempotent and anything the user waits for stays synchronous. |
On This Page
- The Incident
- The Naive Fix, and Why It Fails
- The Pattern
- Walkthrough with Numbers
- Trade-offs: What You Pay
- When NOT to Use It
- Pattern Face-Off: Queue vs Pub/Sub
- In the Wild
- The Interview Lens
- Check Yourself
- Composes With
- TL;DR Card