Module 05 · Communication patterns · Lesson 6 of 10
Message queues
Put the work in a queue and let workers take it when they can. Why decoupling producers from consumers absorbs bursts and survives outages, how acknowledgements and visibility timeouts make processing safe, and what queues do not give you.
Why it matters
The moment a request does more than read and write one record, doing all of it synchronously becomes the bottleneck: the user waits for the email to send, the thumbnail to render and the analytics to update, and a hiccup in any of them fails the whole request. A queue turns "do this now" into "make sure this gets done", which is the single most useful shift in a scaling system. Nearly every design interview has a place for one.
The idea
A message queue is a buffer between a producer, which enqueues messages describing work, and one or more consumers (workers), which dequeue and process them. The producer returns as soon as the message is durably stored. The consumer processes at its own pace. Neither needs the other to be up at the same moment.
What a queue buys you
- 1
Decoupling in time
The producer does not wait for the consumer. Slow work (video encoding, report generation, third-party calls) leaves the request path. - 2
Burst absorption
Traffic spikes become a deeper queue rather than overloaded workers or dropped requests. Workers drain it after the spike. - 3
Failure isolation
If the email provider is down, emails queue up and send later; checkout keeps working. Retries live in the worker, not the user's browser. - 4
Independent scaling
Add workers when the queue grows, remove them when it shrinks; scale producers and consumers on different signals. - 5
Load levelling
A downstream system with a fixed capacity (a database, a partner API with a quota) receives work at the rate it can take.
Acknowledgements and visibility
Dequeuing does not delete a message; it hides it. The consumer processes and then acknowledges, which deletes it. If the consumer crashes before acknowledging, the message becomes visible again after a visibility timeout and another worker picks it up. This is what makes processing safe against worker failure, and it is also why every consumer sees some messages twice: a worker that finishes the work and dies before acking causes a redelivery. Consumers must be idempotent, which the Delivery semantics lesson covers in depth.
Set the visibility timeout longer than the slowest normal processing time, or extend it while working, otherwise a healthy slow worker and a redelivered copy process the same message concurrently.
Queue versus direct call
| Synchronous call | Queue | |
|---|---|---|
| Caller gets | The result | An acknowledgement that the work is stored |
| Latency to result | Immediate | Later; the caller polls, is notified, or does not care |
| Consumer down | Request fails | Work waits |
| Burst | Overload | Deeper queue |
| Complexity | Low | Queue infrastructure, idempotent consumers, monitoring of depth and age |
| Use when | The caller needs the answer now | The work can finish later and must not be lost |
What queues do not give you
They do not make work faster, only later. They do not guarantee exactly-once; at-least-once with idempotent consumers is the realistic contract. Strict global ordering and high parallelism conflict: a queue consumed by many workers delivers roughly in order, not exactly, and FIFO queues with per-key ordering trade throughput for it. A queue that keeps growing is not absorbing a burst, it is hiding a capacity shortfall, which is why queue depth and oldest-message age are the alerts that matter.
Where to use one
Email and notifications, media processing, webhook delivery, search indexing, analytics ingestion, anything calling a third party, and any multi-step workflow where each step can be its own message. The pattern for the caller is: validate synchronously, enqueue, return 202 with a job id, and provide a way to learn the result (polling a status endpoint, SSE, or a callback).
Trade-offs
A queue gives resilience, burst tolerance and independent scaling, and in exchange adds infrastructure, eventual rather than immediate results, duplicate deliveries you must tolerate, and a new thing to monitor. It is the wrong tool when the caller needs the answer in the response, and the right one almost everywhere else.
In an interview
Introduce the queue with the work it removes from the request path: "Checkout writes the order and enqueues order.placed; the API returns in 50 ms. Workers send the confirmation email, update inventory and notify shipping, each idempotent on the order id. Visibility timeout 60 seconds, retries with backoff, dead-letter after five attempts, autoscaling on queue depth." Then the honest caveat: "At-least-once, so every consumer is idempotent."
Check yourself
A worker takes a message, processes it successfully in 45 seconds, and the queue's visibility timeout is 30 seconds. What happens?