Courses › System Design › Core Concepts
Message Queues
Queues vs logs, delivery guarantees, acks, retries, dead-letter queues and backpressure.
A message queue lets one service hand work to another without waiting for it to finish. The producer writes a message and moves on; a consumer picks it up when it's ready. That one idea solves three common problems: absorbing traffic spikes, doing slow work off the request path, and decoupling services so one failing doesn't take down the other.
When to add a queue
- Slow work: sending email, transcoding video, generating reports. Return
202 Acceptedright away and process in the background (Managing Long-Running Tasks). - Bursty traffic: a flash sale produces 50× normal orders for a few minutes. The queue buffers them, and workers drain it at a steady rate.
- Decoupling: the order service doesn't need to know that billing, shipping and analytics all react to an order.
- Retrying flaky work: a failed message goes back on the queue instead of failing the user's request.
api = service "API Servers" @0,1
q = queue "Queue" @1,1
w1 = service "Worker" @2,0.5
w2 = service "Worker" @2,1.5
db = store "Database" @3,1
dlq = queue "Dead-Letter\nQueue" @1,2
api -> q : enqueue, return 202
q -> w1, w2 : pull
w1 -> db
w2 -> db
q --> dlq : after N failuresQueues vs logs
Two designs go by "message queue", and they behave differently:
See Kafka for how partitions and consumer groups work, and Broadcasting with Pub/Sub for delivering one message to many subscribers.
Delivery guarantees
- At most once: a message may be lost, never duplicated. Acceptable for metrics you can afford to drop.
- At least once: never lost, but may be delivered twice — for example, when a worker finishes the job and crashes before acknowledging it. This is what almost every queue gives you.
- Exactly once: only achievable end to end by making the consumer idempotent (or using transactional processing inside one system like Kafka).
[!WARNING] Assume duplicates With at-least-once delivery, design every consumer so that processing a message twice is harmless: use a unique message ID, a database unique constraint, or an upsert. See Safe Retries & Idempotency.
Acknowledgements, retries and dead letters
A consumer acknowledges a message only after it has finished the work. If it doesn't acknowledge within a *visibility timeout*, the queue hands the message to another consumer. A message that keeps failing — a "poison message" — goes to a dead-letter queue after a few attempts, so it stops blocking the rest and someone can inspect it.
Retry with exponential backoff, so a struggling downstream service gets time to recover.
Backpressure and scaling
Watch queue depth and message age. If the queue keeps growing, consumers can't keep up: add workers (autoscale on depth), make processing faster, or push back on producers. A queue hides a capacity problem for a while; it doesn't fix one.
For ordering, remember that more consumers means less ordering. Kafka keeps order within a partition, so pick a partition key (such as user_id) for the things whose order matters, and accept that there's no order across keys.
[!INTERVIEW] Name the guarantee and the failure handling "Uploads publish a job to SQS; transcoding workers pull, process, and ack. Delivery is at-least-once, so the worker skips any video already marked done. After three failures the job goes to a DLQ and we alert" covers the queue, the guarantee, idempotency and poison messages.