Building Blocks · Message Queues and Async Processing

Queues and Asynchronous Processing

Queues versus logs, at-least-once delivery and idempotent consumers, ordering by key, back-pressure, retries and dead-letter queues.

  • 10 min read
  • 11 practice questions
On this page
  1. Description
  2. Work queues and logs
  3. Delivery guarantees
  4. Ordering
  5. Retries, poison messages and dead letters
  6. Back-pressure
  7. The dual-write problem and the outbox
  8. Which tool?
  9. Check yourself
  10. Key takeaways
  11. Quick revise
  12. Practice

Description

Not everything a request triggers has to happen before the user gets an answer. Sign-up, for example, does five things: create the account, send a welcome email, resize the avatar, add the user to the CRM, and log an analytics event. Done one after another inside the request, sign-up takes 3 seconds - and if the email provider has a bad day, sign-up fails. Yet only the first step has to happen before the user sees "Welcome!".

publish: user 42 signed up

200 Welcome! (fast)

Sign-up request

API: create account

Users

Queue

Email worker

Avatar worker

CRM worker

Analytics

publish: user 42 signed up

200 Welcome! (fast)

Sign-up request

API: create account

Users

Queue

Email worker

Avatar worker

CRM worker

Analytics

A message queue lets the request do only what must happen now (create the account), publish a message - "user 42 signed up" - and return. Consumers do the rest at their own pace, retrying as needed. The request is fast, a slow email provider no longer breaks sign-up, and a traffic spike becomes a backlog instead of an outage. In this lesson:

  1. Work queues versus logs (SQS and RabbitMQ versus Kafka).
  2. Delivery guarantees - and why "exactly once" really means idempotent consumers.
  3. Keeping events in order.
  4. Retries, poison messages and dead-letter queues.
  5. Back-pressure when consumers can't keep up.
  6. The outbox: saving data and publishing an event without losing either.

Work queues and logs

Work queue (SQS, RabbitMQ)Log (Kafka, Kinesis)
A message goes toOne consumerEvery consumer group (each reads all messages)
After processingDeletedKept for the retention period
ReplayNoYes - rewind a group's offset
OrderingBest-effort (FIFO queues exist)Strict within a partition
Typical useJobs: send this email, resize this imageEvent streams many services react to

If search indexing, analytics and notifications all need "order placed", a log lets each consume the same stream independently:

Consumer group: analytics

Consumer group: search indexer

Producers

Partition 0

Partition 1

Partition 2

consumer A

consumer B

consumer C

Consumer group: analytics

Consumer group: search indexer

Producers

Partition 0

Partition 1

Partition 2

consumer A

consumer B

consumer C

Within a group, each partition is read by exactly one consumer; each group reads every partition and keeps its own position (offset).

Delivery guarantees

A consumer acknowledges a message to say "done". When it acknowledges decides what can go wrong:

  • At-most-once - acknowledge, then process. A crash mid-processing loses the message.
  • At-least-once - process, then acknowledge. A crash (or a timeout) before the acknowledgment means the message is delivered again - duplicates.
  • Exactly-once as an end-to-end effect comes from at-least-once delivery plus an idempotent consumer: processing the same message twice has the same effect as once. Record processed message IDs (or an idempotency key such as the payment ID) and skip repeats, or make the operation naturally idempotent ("set status = SHIPPED", not "add 1").

A broker's "exactly-once" setting covers its own reads and writes; it can't un-send an email or un-charge a card your consumer already did.

Ordering

Logs are split into partitions for throughput, and order is guaranteed only within a partition. Events that must stay in order - "order created", "order paid", "order shipped" - use the same partition key (the order ID) so they land in one partition, while different orders spread across all partitions. One partition for everything preserves global order but caps throughput at one consumer.

Retries, poison messages and dead letters

Consumers fail - a dependency is down, a bug hits one record. Retry with exponential backoff (1 s, 2 s, 4 s ...) so a struggling dependency isn't hammered. But a poison message - malformed, or hitting a bug - fails every time; retried forever, it blocks everything behind it. After a fixed number of attempts, move it to a dead-letter queue (DLQ): processing continues, an alert fires, and someone inspects and replays it after a fix.

Back-pressure

If producers publish 5,000 messages a second and consumers handle 3,000, the backlog grows by 7.2 million an hour. Eventually messages outlive the retention period and are lost, or the broker's disk fills. Handle it:

  • Watch consumer lag (how far behind the consumers are) and alert on it.
  • Scale consumers on lag - up to the number of partitions, the maximum parallelism of a log.
  • Push back when you can't keep up: slow producers down, reject non-essential work, or shed load at the edge.

The dual-write problem and the outbox

A service saves an order and publishes "order placed". Written as two steps, a crash between them leaves an order with no event (or, reversed, an event for an order that was never saved). The transactional outbox fixes it: in the same database transaction as the order, insert a row into an outbox table. A separate relay reads the outbox and publishes each row, marking it sent - at-least-once, so consumers stay idempotent.

one transaction

read unsent rows

publish

Order service

orders + outbox

Relay

Broker

Consumers (idempotent)

one transaction

read unsent rows

publish

Order service

orders + outbox

Relay

Broker

Consumers (idempotent)

Which tool?

NeedTool
Background jobs, each done once (emails, thumbnails)A work queue
One event, many independent consumers; replayA log (Kafka, Kinesis, Pulsar)
Per-entity orderingA log, partitioned by the entity ID
A job at a future time (reminders, retries later)A delay queue or a scheduler table
Write to the database and publish, reliablyTransactional outbox (or change data capture)
A multi-step business process with compensationsA workflow engine or saga orchestrator

Going further. The deep dive (next tab) covers what Kafka's durability settings really guarantee, sizing partitions and draining a backlog, why retries break ordering, fairness between tenants sharing a queue, evolving event schemas safely, and orchestration versus choreography.

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. A consumer charges a card and crashes before acknowledging. What happens next, and what prevents a double charge?

Show answer

The message is redelivered (at-least-once), and a naive consumer charges again. Pass an idempotency key (the payment ID) to the payment provider, or record processed message IDs and skip repeats.

2. Why can't you just use one Kafka partition to keep all events in order?

Show answer

One partition is consumed by one consumer at a time, so it caps throughput. Usually only events for the same entity need ordering - key by the entity and spread the rest.

3. What is a dead-letter queue for?

Show answer

Messages that keep failing after the retry limit. Moving them aside lets processing continue, and keeps them for inspection and replay once the cause is fixed.

Key takeaways

  • Queue work that can wait: faster requests, isolation from slow dependencies, spikes become backlogs.
  • Work queues hand each message to one consumer; logs keep messages for many groups and replay.
  • Delivery is at-least-once: make consumers idempotent.
  • Order holds within a partition - key related events together.
  • Retry with backoff and a cap; send poison messages to a DLQ.
  • Watch consumer lag; scale consumers or push back on producers.
  • Use a transactional outbox so a write and its event can't half-happen.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before a design interview, and open the full lesson for anything that doesn't ring a bell.

Which tool?

NeedTool
Background jobs, each done once (emails, thumbnails)Work queue (SQS, RabbitMQ)
One event, many independent consumers; replayLog (Kafka, Kinesis, Pulsar)
Per-entity orderingLog partitioned by the entity ID
A job at a future timeDelay queue or a scheduler table
Write to the database and publish, reliablyTransactional outbox (or CDC)
Multi-step process with compensationsWorkflow engine / saga orchestrator (Temporal, Step Functions)
Work queueLog
A message goes toOne consumerEvery consumer group
After processingDeletedKept for retention
ReplayNoYes (rewind the offset)
OrderingBest-effort (FIFO queues exist)Strict within a partition

Delivery

  • At-most-once: ack, then process - a crash loses the message.
  • At-least-once: process, then ack - a crash means a duplicate.
  • Exactly-once effect = at-least-once + an idempotent consumer (record processed IDs, idempotency keys, "set status = SHIPPED" not "add 1"). A broker's exactly-once can't un-send an email.

The traps

  • Doing slow, optional work inside the request.
  • Non-idempotent consumers - redelivery double-charges.
  • One partition "to keep everything in order" - caps throughput at one consumer. Key by entity instead.
  • Retrying a poison message forever - cap retries, then a dead-letter queue (alert, inspect, replay).
  • Ignoring consumer lag - backlogs outlive retention or fill the disk.
  • Two-step "save, then publish" - a crash leaves an order with no event. Use the outbox.

Staff-level points, one line each

  • Kafka durability: replication.factor=3, acks=all, min.insync.replicas=2, unclean.leader.election.enable=false. With min.insync.replicas=1, acks=all silently degrades to acks=1.
  • Compacted topics keep the latest value per key forever - a changelog to rebuild state.
  • Eager rebalances stop the whole group; cooperative ones move only what's needed. Slow processing past max.poll.interval.ms causes rebalance storms.
  • Partitions cap parallelism: 50,000/s at 2,000/s per consumer needs ≥ 25 partitions - plus headroom, since adding partitions remaps keys.
  • Backlog arithmetic: 5,000/s in vs 3,000/s out grows 7.2 M/hour; a 2-hour outage leaves 36 M, which drains in 3.3 h at 8,000/s while 5,000/s keep arriving. Retention must exceed outage + drain time.
  • Retries vs ordering: block the partition (head-of-line blocking), retry topics (break per-key order unless later messages for that key are parked), or version-aware consumers that ignore stale events.
  • Fairness: per-tenant queues with round-robin, enqueue rate limits and priorities, shuffle sharding (2 of 16 workers per tenant: 120 subsets, < 1% chance two tenants share the same pair).
  • Schema evolution: add fields with defaults (backward), ignore unknown fields (forward); rename/remove = add new, migrate, stop old. A schema registry fails breaking changes in CI; compacted/long-retention topics hold every old version.
  • Choreography (services react to events) stays decoupled but hides the process; orchestration (a workflow engine) makes it visible, retried and compensable. Events for facts, orchestrators for processes.

Say it in an interview

  • "Sign-up only creates the account; everything else is a message, so a slow email provider can't break it."
  • "Delivery is at-least-once, so the payment consumer passes the payment ID as an idempotency key."
  • "Order events are keyed by order ID - ordered per order, spread across partitions."
  • "A 2-hour outage takes over 3 hours to drain, so retention is set to days, not hours."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Queues and Asynchronous Processing free

Practice Queues and Asynchronous Processing free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime