Week 8: Message Queues & Asynchronous Processing

Weeks 6–7 put a cache in front of the database so reads don't hit it directly; this week puts a queue in front of slow or unreliable work so writes and background jobs don't have to happen synchronously either. You'll learn when to reach for a point-to-point work queue versus a pub-sub event log, how decoupling producers from consumers changes a system's failure characteristics, and why "at-least-once delivery plus an idempotent consumer" is the answer nearly every real interview lands on. The consistency questions this raises about ordering and duplicate processing set up Weeks 9–10's deeper treatment of distributed consistency and consensus.

Module 6 of 24 Week 8 of 28 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Choose between point-to-point and pub-sub messaging models for a given use case, and justify it
  • Explain how decoupling producers from consumers changes failure and scaling behavior, and design for backpressure
  • Reason about at-least-once vs. exactly-once delivery and design an idempotent consumer

1. Point-to-Point vs. Pub-Sub Messaging

A point-to-point queue (RabbitMQ, SQS-style) hands each message to exactly one consumer out of a pool of competing consumers, then removes it once it's acknowledged. This is the classic "work queue" pattern: a producer drops a job on the queue, any one of N worker processes picks it up, and the work happens once. It's the right model when a message represents a unit of work that should be done a single time — resize this image, send this email, charge this card.

A pub-sub log (Kafka-style) instead treats messages as an ordered, durable, append-only stream that multiple independent consumers can each read in full. Kafka doesn't delete a message once one consumer reads it — it retains messages for a configured retention window (hours to indefinitely) regardless of consumption, and each consumer group tracks its own offset into the log. That means the same "order placed" event can be independently consumed by an inventory service, a notification service, and an analytics pipeline, each reading the full stream at its own pace, none of them stealing the message from the others.

point-to-point vs. pub-sub, side by side
Point-to-point (work queue -- RabbitMQ / SQS style):

  [Producer] --> [ Queue ] --> one of [Worker A] [Worker B] [Worker C]
                                (message consumed once, then removed)

Pub-sub (log -- Kafka style):

  [Producer] --> [ Topic / Log, partitioned, retained ]
                        |-- read by --> [Consumer Group: Inventory]
                        |-- read by --> [Consumer Group: Notifications]
                        |-- read by --> [Consumer Group: Analytics]
                 (each group reads the whole stream independently,
                  tracking its own offset)

The practical decision usually comes down to one question: does exactly one part of the system need to act on this message, or could several unrelated parts each need to react to the same event independently? "Resize this uploaded photo" is a job for a work queue — you don't want three workers all resizing the same photo. "A user placed an order" is an event several independent systems care about, which is what pub-sub is built for.

Name the products, not just the patterns

In an interview, saying "I'd use a message queue here" is weaker than "I'd use SQS-style point-to-point queueing for the resize jobs, since each job should run once, but Kafka-style pub-sub for the order-placed event, since inventory, notifications, and analytics all need to react independently." Naming the pattern and a real product that implements it signals you understand the tradeoff, not just the vocabulary.

2. Decoupling Producers from Consumers

A queue's most important structural effect is temporal decoupling: the producer and consumer no longer need to be available at the same moment. A web server handling an upload can enqueue a "process this video" job and return a response to the user in milliseconds, without waiting for transcoding to finish and without caring whether the transcoding workers are up, overloaded, or mid-deploy at that exact instant. If the consumers are briefly down, messages simply wait in the queue instead of failing the request outright.

The second effect is load leveling: traffic to a system is rarely smooth (recall Week 1's point that peak load often runs 2–3x average), but a queue lets you absorb a spike as a growing queue depth rather than a spike in database or CPU load. Consumers keep draining the queue at a steady, sustainable rate — the burst is smoothed out rather than passed straight through to whatever is doing the actual work.

load leveling, worked example
Signups spike 10x during a marketing push:
  Normal:  50 signups/sec  ->  50 "send welcome email" jobs/sec
  Spike:  500 signups/sec  -> 500 "send welcome email" jobs/sec

Without a queue: email service must scale to handle 500/sec
                  immediately, or requests start failing.

With a queue:     web servers enqueue 500 jobs/sec (cheap, fast)
                  email workers keep consuming at their steady
                  rate, say 80/sec per worker
                  queue depth grows during the spike, drains
                  afterward -- users get their email a little
                  late, not not at all, and no request fails

Decoupling also isolates failures. If the email-sending service has an outage, the signup flow itself keeps working — jobs simply accumulate in the queue until the service recovers, instead of the outage cascading backward into a user-facing failure. This is the same instinct as Week 3's discussion of stateless services: a queue lets producers and consumers scale, deploy, and fail independently of each other.

Decoupling isn't free — it costs you synchronous certainty

A queue trades an immediate, certain result for eventual, asynchronous completion. That's the right trade for a welcome email or a thumbnail, but the wrong trade for "did the payment succeed?" — the user needs that answer synchronously. Good candidates explicitly identify which parts of a flow can be async and which can't, rather than queueing everything by default.

3. Backpressure & Flow Control

Decoupling has a failure mode of its own: if producers sustainably outpace consumers, the queue doesn't magically absorb the difference forever — it grows without bound, latency for every message climbs, and eventually the broker runs out of memory or disk. Backpressure is the set of mechanisms that keep that growth in check instead of discovering it in an incident.

when consumers can't keep up — worked example
Producers enqueue:            5,000 messages/sec
Each consumer processes:      200 messages/sec
Consumers running:            20
Total consumer throughput:    20 * 200 = 4,000 messages/sec

Net queue growth:             5,000 - 4,000 = 1,000 messages/sec
After 10 minutes (600s):      600,000 messages backed up

To merely keep pace (not drain the backlog), you need:
  5,000 / 200 = 25 consumers minimum

Common backpressure strategies, usually combined: autoscale the consumer pool on queue depth or consumer lag (the number of unprocessed messages a consumer group is behind) rather than CPU alone, since a slow downstream dependency can starve CPU-idle consumers; bound the queue and have producers block, retry with backoff, or shed load once it's full rather than growing unboundedly; and route messages that repeatedly fail processing to a dead-letter queue so one poison message can't block or endlessly retry against the rest of the stream.

Consumer lag is the metric, not queue length alone

A raw queue depth of 10,000 means nothing on its own — it could be one second of normal traffic or an hour of sustained overload, depending on throughput. Consumer lag (how far behind consumers are, in time or in message count relative to production rate) is what production systems actually alert on, and it's a stronger answer to give in an interview than "I'd monitor the queue size."

4. Delivery Guarantees & Idempotency

Message queues offer one of three delivery guarantees. At-most-once means a message is delivered zero or one times — the consumer acknowledges before processing, so a crash mid-processing loses the message forever; this is rarely what you want. At-least-once means a message is delivered one or more times — the consumer acknowledges only after processing completes, so if it crashes or the ack is lost in transit, the broker redelivers the same message; this is the default nearly every real system chooses, and it means consumers will occasionally see duplicates. Exactly-once — delivered precisely one time, with no loss and no duplication — sounds ideal but is genuinely hard to guarantee end-to-end across independent systems; where it's offered (e.g. Kafka's transactional producer/consumer APIs within a single Kafka cluster), it usually comes with real constraints and cost, and doesn't extend for free across arbitrary external side effects like an HTTP call to a third-party payment API.

The practical, interview-safe answer is: design for at-least-once delivery, and make the consumer idempotent — processing the same message twice should produce the same end state as processing it once. That turns the hard distributed problem (exactly-once delivery) into an easier local one (idempotent processing).

idempotent consumer, worked pseudocode
function handleMessage(message):
  # message.id is a stable, producer-assigned unique key
  if processedMessageIds.contains(message.id):
      ack(message)          # already handled -- just acknowledge and skip
      return

  beginTransaction()
    applyEffect(message)                 # e.g. UPSERT, not blind INSERT
    processedMessageIds.insert(message.id)
  commitTransaction()

  ack(message)

Two techniques do most of the work in practice: storing a processed-message ID (or idempotency key) alongside the effect in the same transaction, so a redelivery is recognized and skipped; and preferring naturally idempotent operations — an UPSERT keyed on order ID instead of a blind INSERT, or "set the balance to $50" instead of "add $10" — since some operations are safe to repeat by construction and need no dedup table at all.

"Exactly-once" is a trap phrase — reframe it

If an interviewer asks for exactly-once delivery, the strongest response isn't "yes, easy" — it's naming the real constraint: true exactly-once across independent systems isn't generally achievable, so the standard approach is at-least-once delivery plus an idempotent consumer, which produces exactly-once effects even though the message itself might be delivered more than once.

5. Hands-on Exercise

Hands-on

Design the async pipeline for photo processing

A photo-sharing app needs to generate three thumbnail sizes and run content-moderation on every uploaded photo, without making the uploader wait for any of that to finish.

Requirements:

  1. Choose point-to-point or pub-sub (or a combination) for this pipeline and justify the choice in 2–3 sentences, referencing Section 1's distinction.
  2. Sketch the producer → queue/topic → consumer(s) flow as a text diagram, showing where the upload request returns to the user relative to where processing happens.
  3. Assume uploads run at 200/sec on average with 4x spikes during peak evening hours; state a backpressure strategy (Section 3) and how many consumers you'd need at peak if each consumer processes 50 photos/sec.
  4. State the delivery guarantee you're designing for and describe, concretely, how you'd make thumbnail generation idempotent (what key would you check, what would a duplicate delivery actually do).
  5. Describe what should happen to a photo that fails moderation processing 5 times in a row.
Hint

Thumbnail generation and content moderation are two different consumers reacting to the same "photo uploaded" event, which is a strong signal for one of the two models from Section 1 — think about whether they should compete for the same messages or each see every message independently.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

You need three independent services to each react to "user signed up." Why is a Kafka-style pub-sub log a better fit than an SQS-style work queue here?

A point-to-point queue delivers each message to exactly one consumer out of a competing pool, then removes it — the second and third services would never see it. A pub-sub log retains the event and lets each consumer group read the full stream independently at its own offset, so all three services can each process every "user signed up" event without stealing it from one another.

Q2

Why does decoupling producers and consumers with a queue improve failure isolation, not just throughput?

Without a queue, a consumer-side outage propagates directly backward — a caller waiting synchronously on a dead dependency fails too. With a queue in between, a producer can keep enqueueing successfully even while consumers are entirely down; messages simply accumulate and get processed once consumers recover, so one side's outage doesn't automatically become the other side's outage.

Q3

Why is consumer lag a better signal to alert on than raw queue depth?

Queue depth alone has no fixed meaning — 10,000 messages could be a routine burst that drains in a second at high throughput, or a sign of consumers stalled for an hour at low throughput. Consumer lag expresses how far behind consumption is relative to production, which is the number that actually tells you whether the system is falling behind and by how much, independent of the raw count.

Q4

An interviewer asks you to guarantee exactly-once processing. What's the strongest way to respond?

Point out that true exactly-once delivery is generally not achievable across independent distributed systems, then propose the standard practical alternative: design for at-least-once delivery and make the consumer idempotent, typically via a processed-message ID check or a naturally idempotent operation like an UPSERT. That produces exactly-once effects even though the underlying message may occasionally be redelivered — which is what interviewers are actually listening for.