Message Queues and Streams

Move slow work off the request path: queues vs logs, delivery guarantees, ordering with partitions, consumer groups, dead letters and backpressure.

17 minLesson 3 of 8
On this page
  1. The idea
  2. Queue or log
  3. Delivery guarantees
  4. Ordering, partitions and consumer groups
  5. Backpressure
  6. Numbers worth remembering
  7. In the interview
  8. Common mistakes
  9. What to practise next

A signup endpoint creates the account, then calls an email provider to send a welcome message before it responds. One afternoon the provider slows from 100 ms to 3 seconds. At 50 signups a second, 50 × 3 = 150 requests are now stuck waiting at any moment, the server's worker pool fills up, and signups fail even though the account database is fine.

The welcome email did not need to happen before the response. Put it on a queue, answer the user in milliseconds, and let a separate worker send it whenever the provider allows.

The idea

A producer writes a message to a durable broker and moves on. A consumer reads it later and does the work. The two no longer share a speed or a fate: a burst of messages waits in the broker instead of crushing the consumer, and a dead consumer delays work instead of failing requests.

The costs: the work happens later (seconds, sometimes minutes), there is one more system to run, and messages can arrive twice.

The API only writes a message. Two consumer groups each get every message; failed messages end up in a dead-letter queue.Signup API to signups topic (publish), asynchronous; signups topic to Email workers, asynchronous; signups topic to Analytics workers, asynchronous; Email workers to Email provider (send); Email workers to Dead letters (after 5 failures).Signup APIsignups topicEmail workersAnalyticsworkersEmail providerDead letterspublishsendafter 5 failures

request and replyasynchronous message

The API only writes a message. Two consumer groups each get every message; failed messages end up in a dead-letter queue.

Queue or log

Brokers come in two shapes. A queue (such as RabbitMQ or SQS) hands each message to one consumer and deletes it once acknowledged. A log (such as Kafka) appends messages and keeps them for days; each consumer group remembers its own position (an offset) and can rewind.

Queue vs log
OptionGood atBad atPick it when
Queue Usual defaultSimple job distribution. Per-message acks and retries. Add workers and they share the load.A message is gone once processed: no replay, and a second kind of consumer needs its own copy.Background jobs: emails, thumbnails, webhooks.
Log (stream)Many independent consumers read the same events. Replay after a bug. Order kept per partition.Parallelism is capped by the partition count. One bad message holds up its partition.Events several systems react to (orders, clicks), or anything you may need to reprocess.

One job type and one kind of worker: use a queue. Once a second team wants the same events, or you need replay, use a log.

Delivery guarantees

A consumer does two things: the work, and the acknowledgement. A crash can land between them, and the order you choose decides what a crash costs.

  • At-most-once: ack, then work. A crash after the ack loses the message. Fine for metrics you can afford to drop.
  • At-least-once: work, then ack. A crash before the ack means the message comes back and the work runs again. The usual choice.
  • "Exactly-once": no broker can stop a consumer from doing an external action twice. What you can build is exactly-once effect: at-least-once delivery plus processing that is idempotent, so the second run changes nothing. Some brokers offer exactly-once within their own reads and writes; it does not cover your email provider.
At-least-once: a crash, a redelivery, a poison messageUse the arrows or ← → keys
  1. At-least-once: a crash, a redelivery, a poison message, step 1Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobsjob 81Worker 1Worker 2Email provider
    emails sent
    0

    1A user signs up. The API stores job 81 in the queue and responds in about 5 ms. The provider's speed no longer matters to the user.

  2. At-least-once: a crash, a redelivery, a poison message, step 2Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobs81 hidden 30 sWorker 1Worker 2Email provider
    emails sent
    0

    2Worker 1 receives job 81. The queue hides it from other workers for 30 seconds while it works.

  3. At-least-once: a crash, a redelivery, a poison message, step 3Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobsWorker 1Worker 2Email provider
    emails sent
    1

    3Worker 1 asks the provider to send, passing job-81 as an idempotency key (this provider accepts one). The email goes out.

  4. At-least-once: a crash, a redelivery, a poison message, step 4Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobs81 not ackedWorker 1downWorker 2Email provider
    emails sent
    1

    4Worker 1 crashes before it acks. The queue cannot tell this from a slow worker, so it keeps the job.

  5. At-least-once: a crash, a redelivery, a poison message, step 5Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobs81 redeliveredWorker 1downWorker 2Email provider
    emails sent
    1

    5After 30 seconds job 81 becomes visible again and goes to Worker 2. This is the duplicate that at-least-once promises.

  6. At-least-once: a crash, a redelivery, a poison message, step 6Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobsWorker 1downWorker 2Email providerdup job-81
    emails sent
    1

    6Worker 2 sends with the same key. The provider has seen job-81 and does not send again. Worker 2 acks and the job is deleted.

  7. At-least-once: a crash, a redelivery, a poison message, step 7Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobsWorker 1downWorker 2job 97 failsEmail provider
    emails sent
    1
    attempts on 97
    5

    7Job 97 has a malformed address. It fails on every attempt. Without a limit it would be retried forever and waste a worker each time.

  8. At-least-once: a crash, a redelivery, a poison message, step 8Signup API to Email jobs, asynchronous; Email jobs to Worker 1; Email jobs to Worker 2; Worker 1 to Email provider; Worker 2 to Email provider.Signup APIEmail jobs97 dead-letteredWorker 1downWorker 2Email provider
    emails sent
    1
    attempts on 97
    5

    8After 5 attempts the queue moves job 97 to a dead-letter queue and an alert fires. Someone fixes the data and replays it.

1 / 8

Ordering, partitions and consumer groups

A log is split into partitions. Order is guaranteed only inside one partition. The producer picks the partition by hashing a key, so choose the key whose events must stay in order: with order_id as the key, "created", "paid" and "shipped" for one order arrive in sequence, while different orders spread across partitions.

A consumer group is one logical subscriber. Inside a group, each partition is read by exactly one consumer at a time, so a topic with 12 partitions supports at most 12 busy consumers in a group. Different groups (email, analytics) each read every message independently. Pick the partition count for the parallelism you will need later; adding partitions afterwards remaps keys and breaks per-key order during the change.

A hot key, one huge customer, lands entirely on one partition. If that is a risk, add a suffix to spread it, and give up ordering for that key.

Backpressure

A queue absorbs bursts; it does not fix a consumer that is slower than its producers on average. If producers write 1,200 messages a second and consumers handle 1,000, the backlog grows by 200 a second, 720,000 an hour, and each message waits longer. Watch consumer lag (messages, or seconds, behind) and act on it:

  • Scale consumers, up to the partition count.
  • Bound the queue and push back on producers: return 429 or 503 and let clients retry later.
  • Shed or sample low-value messages (debug events) before high-value ones (payments).
Estimate: Draining a backlog

The backlog reached 720,000 messages. You scale consumers to 1,500 a second while producers keep writing 1,200 a second. How long until the backlog is empty?

You know
  • Backlog: 720,000 messages
  • Producers: 1,200 a second
  • Consumers after scaling: 1,500 a second

Show the working
  1. Net drain rate: 1,500 − 1,200 = 300 a second
  2. 720,000 ÷ 300 = 2,400 seconds
  3. 2,400 ÷ 60 = 40 minutes

Estimate 40 minutes (anything from 15 minutes to 2 hours is a good answer)

Draining is only as fast as your spare capacity. With 10% headroom (1,320 a second) it would take 100 minutes.

Numbers worth remembering

  • Publishing to a broker in the same data center: a few ms, including replication for durability.
  • One partition handles on the order of thousands to tens of thousands of small messages a second; total throughput scales with partitions.
  • Little's law: messages waiting = arrival rate × wait time. 50 a second × 3 s = 150, the hook's stuck requests.

In the interview

Bring in a queue when work is slow, flaky or not needed for the response (emails, thumbnails, fan-out), when traffic is bursty, or when several systems react to one event. Then say the guarantee and how you handle its cost: "The click events go to a log partitioned by link id. Delivery is at-least-once, so the counter worker dedupes by event id."

Sending order emails
Interviewer

After checkout we send a confirmation email and update analytics. Where does that happen?

You

Not in the checkout request. Checkout publishes an OrderPlaced event to a log keyed by order id. Email and analytics are separate consumer groups, so a slow email provider never delays analytics or checkout.

Interviewer

Can a customer get two emails?

You

With at-least-once delivery, yes, unless I prevent it. The email worker passes the event id as an idempotency key to the provider, so a redelivered event sends nothing. Messages that fail five times go to a dead-letter queue with an alert.

Follow-ups you should expect:

  • "How do you keep events for one user in order?" Key by user id so they share a partition, and process each partition with one consumer.
  • "What if consumers fall behind for an hour?" Alert on lag, scale consumers up to the partition count, and work out drain time from the spare capacity.
  • "The API crashes after creating the account but before publishing. Now what?" No welcome email, ever. Write the message to an outbox table in the same transaction as the account (see transactions).
  • "What if the provider has no idempotency key?" Record sent job ids in your own table. A crash between sending and recording can still send twice, so make that window short.

Common mistakes

Quick check

A consumer acks a message, then processes it, and crashes in between. What happened to that message?

A topic has 8 partitions. You run 12 consumers in one group. How many are busy?

Producers write 2,000 messages a second and consumers process 1,800. How large is the backlog after 10 minutes?

What to practise next

Every consumer here relies on idempotency and retries, so read that next. To see a queue carry a whole design, work through Design a Notification System.