Back to the map

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.

CoreMust-knowJunior 11 minVideo Updated Nov 2026
Video walkthrough · 2:15 · narrated, with captions

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.

APIproducerenqueue, return 202Queuedurable, ordered-ishpullpullWorker 1Worker 2queue depth is the pressure gauge; scale workers on it
The API enqueues and responds immediately. Workers pull at their own rate; a burst becomes a longer queue, not a failed request.

What a queue buys you

  1. 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. 2

    Burst absorption

    Traffic spikes become a deeper queue rather than overloaded workers or dropped requests. Workers drain it after the spike.
  3. 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. 4

    Independent scaling

    Add workers when the queue grows, remove them when it shrinks; scale producers and consumers on different signals.
  5. 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 callQueue
Caller getsThe resultAn acknowledgement that the work is stored
Latency to resultImmediateLater; the caller polls, is notified, or does not care
Consumer downRequest failsWork waits
BurstOverloadDeeper queue
ComplexityLowQueue infrastructure, idempotent consumers, monitoring of depth and age
Use whenThe caller needs the answer nowThe 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?

Message queues · Backplane