๐Ÿ“HowToHLD
Vote for New Content
Vote for New Content
Home/High Level Design/Concepts

Message queues

Learn how message queues decouple services, which delivery guarantee fits your workload, and how to build a queue layer that survives consumer failures.

41 min read2026-03-24mediummessage-queueskafkaasyncdecouplinghld

TL;DR

  • A message queue is an async communication intermediary that decouples producers from consumers โ€” producers write messages to a durable broker, consumers read and process them independently, on their own schedule.
  • The core trade-off is resilience and decoupling vs. operational complexity and eventual consistency: your system becomes more fault-tolerant but harder to reason about end-to-end.
  • At-least-once delivery is the default guarantee. Producers retry until acknowledged, so consumers must be idempotent โ€” processing the same message twice should produce the same result as processing it once.
  • Use queues when downstream work takes >200ms, when services need to decouple failure domains, or when traffic spikes threaten a downstream service you don't own.
  • Kafka for high-throughput event streaming with replay; RabbitMQ/SQS for task queues and command routing โ€” they are not interchangeable, and reaching for Kafka by default is one of System Design's most common over-engineering mistakes.

The Problem It Solves

It's Black Friday. Your checkout API synchronously calls four services: inventory update (100ms), confirmation email (300ms), analytics event (150ms), push notification (200ms). That's 750ms of blocking wait per checkout. For months this worked fine in staging. Under production Black Friday traffic, the confirmation email service hits its Sendgrid rate limit and response times climb from 300ms to 5,000ms.

Your checkout API โ€” waiting synchronously on the email call โ€” starts timing out. Your load balancer returns 503s. Every single checkout on your platform goes down. Not because your service is broken. Because the email service had a bad afternoon.

The hidden coupling in every synchronous architecture

Synchronous service calls create implicit availability chains: your endpoint is only as available as the least-reliable service you call. Call four services with 99.9% uptime each and your composed availability drops to roughly 99.9%โด โ‰ˆ 99.6% โ€” about 35 hours of downtime per year, not because your service broke, but because something downstream did.

API server making four synchronous downstream calls to inventory, email, analytics, and notification services, with a total latency of 750ms and a failure annotation showing that any single slow or failed service blocks the entire checkout.
Any single downstream service going slow or down blocks the entire checkout flow. Your availability is the product of every downstream service's availability โ€” and none of that failure was your code's fault.

The problem is not that the email service is slow. It's that you made checkout success conditional on email delivery. Those are different concerns โ€” they don't need to complete in the same HTTP response cycle. I've seen this exact scenario take down a checkout page on Cyber Monday โ€” not because our service was broken, but because a third-party email provider started degrading at 10 AM.


What Is It?

A message queue is an async communication pattern where a producer writes a message to an intermediary broker, and one or more consumers read and process it independently, on their own schedule. The broker persists the message until a consumer acknowledges successful processing.

Analogy: A restaurant kitchen. When a waiter takes an order, they don't stand at the table until the chef finishes cooking. They walk the ticket to the kitchen, pin it to the rail, and go take the next order. The kitchen is the queue. The ticket is the message. The chef is the consumer. The waiter (your HTTP API) is free the moment the ticket hits the rail โ€” the kitchen's backlog doesn't block the front-of-house.

That separation is the fundamental insight. The checkout API's job is to record intent: "customer X bought product Y for $Z." What happens next โ€” reducing inventory, sending an email, updating analytics โ€” can happen asynchronously, in parallel, at the consumers' own pace.

API server publishing a checkout.completed event to a message queue in under 5ms and immediately returning HTTP 200, while four separate worker services consume the message asynchronously in the background. A dead letter queue captures messages that exceed three retry attempts.
The API publishes one event and returns 200 OK in under 10ms. Workers consume independently โ€” the email service going down doesn't affect inventory processing or the user's checkout experience. The queue buffers traffic spikes and retries on consumer failure.

With a queue in place, the email service can be slow, down, or mid-deployment without any user feeling it. The checkout succeeds. The email message sits in the queue until the email service recovers and drains the backlog โ€” not a single email is lost.


How It Works

Here's what happens on every checkout request when a message queue is in use:

  1. Producer publishes a message โ€” The checkout API validates the order, writes it to the database, then publishes a checkout.completed event to the broker. This takes < 5ms. The API returns HTTP 200 immediately.
  2. Broker persists the message โ€” The broker durably stores the message on disk. Even if every consumer crashes right now, the message is safe. The broker owns the durability contract.
  3. Consumer polls or receives โ€” Consumers either pull messages (Kafka, SQS, Redis Streams) or receive pushed deliveries (RabbitMQ push mode). Either way, the consumer independently fetches the next message.
  4. Consumer processes the message โ€” The email worker reads the event, calls Sendgrid, and sends the email. This takes however long it takes โ€” 300ms normally, 30 seconds during degradation. The user's HTTP response is completely unaffected.
  5. Consumer ACKs the message โ€” On successful processing, the consumer sends an acknowledgment. The broker marks the message consumed and removes it from the active queue.
  6. On failure: NACK + retry โ€” If the consumer crashes or fails before ACKing, the message re-appears after the visibility timeout expires. After N retries, it routes to the Dead Letter Queue (DLQ).
// producer.ts โ€” Checkout API publishes an event on successful order creation
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({ region: "us-east-1" });

async function checkout(order: Order): Promise<void> {
  // Step 1: Write to DB โ€” source of truth first
  const savedOrder = await db.orders.create(order);

  // Step 2: Publish event โ€” fire-and-forget, < 5ms
  // โš ๏ธ Simplified for clarity. In production, use the Transactional Outbox pattern
  // (see Q5 in Test Your Understanding) to guarantee the event is published
  // even if this process crashes between the DB write and this send call.
  await sqs.send(new SendMessageCommand({
    QueueUrl: process.env.CHECKOUT_QUEUE_URL,
    MessageBody: JSON.stringify({
      type: "checkout.completed",
      orderId: savedOrder.id,
      customerId: savedOrder.customerId,
      totalCents: savedOrder.totalCents,
      publishedAt: new Date().toISOString(),
    }),
    // MessageGroupId requires a FIFO queue (URL ending in .fifo)
    // Remove this line for SQS Standard queues or you'll get InvalidParameterValue
    // For FIFO queues: also supply MessageDeduplicationId per message, OR enable
    // ContentBasedDeduplication on the queue at creation time โ€” otherwise SQS rejects the send
    MessageGroupId: savedOrder.customerId,
  }));

  // Step 3: Return immediately โ€” no waiting on workers
  return; // 200 OK โ€” user sees confirmed checkout instantly
}
// email-worker.ts โ€” Consumes checkout events and sends confirmation emails
import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({ region: "us-east-1" });

async function processNextBatch(): Promise<void> {
  const response = await sqs.send(new ReceiveMessageCommand({
    QueueUrl: process.env.CHECKOUT_QUEUE_URL,
    MaxNumberOfMessages: 10,
    WaitTimeSeconds: 20,       // Long-polling: fewer empty receives, lower cost
    VisibilityTimeout: 60,     // 60s to process before SQS assumes worker crashed
  }));

  for (const msg of response.Messages ?? []) {
    const event = JSON.parse(msg.Body!);
    try {
      await sendConfirmationEmail(event.customerId, event.orderId);
      // Only delete AFTER successful processing โ€” this is the ACK
      await sqs.send(new DeleteMessageCommand({
        QueueUrl: process.env.CHECKOUT_QUEUE_URL,
        ReceiptHandle: msg.ReceiptHandle!,
      }));
    } catch (err) {
      // Do NOT delete โ€” SQS redelivers after VisibilityTimeout expires
      console.error("Email worker failed, message will be redelivered", err);
    }
  }
}

Interview tip: narrate the ACK/NACK contract explicitly

When you draw a queue in an interview, always follow it with: "If the email worker crashes before ACKing, SQS redelivers the message after the visibility timeout expires โ€” nothing is ever lost. After three retries it routes to the DLQ where we alert on-call." That one sentence earns significantly more than drawing the queue box alone.

sequenceDiagram
    participant C as ๐Ÿ‘ค Client
    participant A as โš™๏ธ API Server
    participant B as ๐Ÿ“จ Message Broker
    participant W as โš™๏ธ Email Worker
    participant D as โ˜ ๏ธ Dead Letter Queue

    Note over C,D: Happy path<br/>Worker processes successfully
    C->>A: POST /checkout
    A->>B: Publish checkout.completed ยท < 5ms
    B-->>A: Message persisted and accepted
    A-->>C: HTTP 200 ยท order confirmed
    Note over B,W: Async after HTTP response โ€” not on the critical path
    B->>W: Deliver message
    activate W
    W->>W: sendEmail() ยท ~300ms
    W-->>B: ACK โ€” DeleteMessage
    deactivate W
    B->>B: Message removed from queue

    Note over C,D: Failure path<br/>Worker crashes mid-send
    B->>W: Redeliver (visibility timeout expired)
    activate W
    W-->>B: NACK (or no ACK before timeout)
    deactivate W
    B->>B: Retry count incremented
    Note over B,D: After max_receive_count exceeded
    B->>D: Route to Dead Letter Queue ยท alert fires

Key Components

ComponentRole
ProducerThe service that creates and publishes messages. Producers don't know how many consumers exist or when messages will be processed.
ConsumerThe service that reads and processes messages. Multiple consumers can read from the same queue simultaneously (competing consumers pattern).
BrokerThe intermediary that receives, stores, and delivers messages. Examples: Kafka, RabbitMQ, Amazon SQS, Redis Streams. The broker owns durability.
Queue / TopicA named channel within the broker. A queue delivers each message to exactly one consumer (work dispatch). A topic broadcasts to all subscribers (fan-out).
MessageA discrete unit of work โ€” typically a JSON payload with a type, ID, and data. Messages should be small (< 256KB) and self-contained.
Acknowledgment (ACK)The consumer's signal that a message was processed successfully. Without an ACK, the broker redelivers. The ACK is what makes at-least-once delivery safe.
Visibility TimeoutThe window during which a delivered message is hidden from other consumers. If the consumer doesn't ACK within this window, the message becomes visible again for redelivery. SQS default: 30 seconds.
Dead Letter Queue (DLQ)A separate queue where messages land after exceeding the maximum retry count. Every DLQ message is a bug or misconfiguration to investigate โ€” never silently drop DLQ messages.
Consumer GroupA logical grouping of consumers sharing the processing load of a queue or topic. Each message is delivered to exactly one member of the group, enabling horizontal scale.
PartitionA Kafka concept โ€” a topic is split into N ordered partitions, each consumed by at most one consumer in a group at a time. Partitions are the unit of parallelism in Kafka, and partition count caps your consumer group scaling ceiling.

Types / Variations

Point-to-Point (Queue)

Each message is consumed by exactly one consumer. If you have five email worker instances polling the same queue, each checkout event is picked up by exactly one of them. This is the right model for distributing work โ€” order processing, email sending, invoice generation, background jobs.

flowchart LR
  subgraph Producers["๐Ÿ“ค Producers"]
    P1["โš™๏ธ Checkout Service"]
    P2["โš™๏ธ Refund Service"]
  end

  subgraph QueueTier["๐Ÿ“จ Queue (Point-to-Point)"]
    Q["๐Ÿ“ฌ checkout-events\nEach message โ†’ one consumer only"]
  end

  subgraph Workers["๐Ÿ“ฅ Competing Consumers"]
    C1["โš™๏ธ Email Worker 1"]
    C2["โš™๏ธ Email Worker 2"]
    C3["โš™๏ธ Email Worker 3"]
  end

  P1 -->|"Publish message"| Q
  P2 -->|"Publish message"| Q
  Q -->|"Deliver to exactly one"| C1
  Q -->|"Deliver to exactly one"| C2
  Q -->|"Deliver to exactly one"| C3

Publish/Subscribe (Topic Fan-out)

Each message is delivered to every subscriber independently. When checkout publishes checkout.completed, the email service, analytics service, inventory service, and notification service each receive their own copy and process it independently. This is the right model for broadcasting events โ€” one write, N reactions.

flowchart LR
  subgraph Producer["๐Ÿ“ค Producer"]
    P["โš™๏ธ Checkout Service"]
  end

  subgraph TopicTier["๐Ÿ“ก Topic (Pub/Sub Fan-out)"]
    T["๐Ÿ“ข checkout.completed\nOne publish โ†’ N independent copies"]
  end

  subgraph Subscribers["๐Ÿ“ฅ Independent Subscribers"]
    S1["๐Ÿ“ง Email Service"]
    S2["๐Ÿ“ฆ Inventory Service"]
    S3["๐Ÿ“Š Analytics Service"]
    S4["๐Ÿ“ฑ Notification Service"]
  end

  P -->|"Publish once"| T
  T -->|"Independent copy"| S1
  T -->|"Independent copy"| S2
  T -->|"Independent copy"| S3
  T -->|"Independent copy"| S4

Push vs. Pull Consumption

ModelHow it worksBest forExample
PullConsumer polls the broker on its own scheduleControlled throughput ยท backpressure by designKafka, SQS, Redis Streams
PushBroker delivers messages to consumer endpointLow-latency, event-driven processingRabbitMQ, webhooks

Pull is the safer default โ€” consumers control their ingestion rate and can never be overwhelmed. Push is faster end-to-end but requires explicit backpressure handling to avoid consumer crashes under high throughput.

Kafka vs. RabbitMQ vs. Amazon SQS

DimensionKafkaRabbitMQAmazon SQS
Primary modelDistributed append-only logMessage broker with routingFully managed simple queue
ThroughputMillions of msgs/sec per clusterHundreds of thousands/secNearly unlimited (Standard); 3,000/sec with batching (FIFO)
Message retentionDays to weeks (configurable)Until ACKed โ€” no replay4 days default (14 max)
ReplayYes โ€” rewind offset by timestampNoNo
OrderingPer-partition guaranteedFIFO queues availableFIFO queues available
Operational complexityHigh (KRaft; ZooKeeper removed in Kafka 4.0)Medium (cluster + plugins)Zero (fully managed)
Best forEvent streaming, audit logs, data pipelinesTask queues, RPC patterns, complex routingSimple task queuing in AWS workloads
Not forSimple task queues needing minimal opsHigh-throughput streamingReplay, complex routing

My recommendation: default to SQS for AWS-native workloads, RabbitMQ when you need complex exchange/routing logic, and Kafka only when high-throughput event streaming with replay is a real requirement. Reaching for Kafka because "it's powerful" without a concrete replay or throughput need is the most common over-engineering trap in system design interviews.


Delivery Guarantees

This is the section most candidates skip over. It's also where interviewers immediately find out how deeply you understand async systems.

At-Most-Once

The producer fires a message and never retries. If the broker is down or the consumer crashes, the message is lost forever. The upside: no duplicate processing. The downside: data loss is silent and statistically guaranteed to happen eventually.

Use when: Telemetry you can afford to lose โ€” click events, page view counts, heartbeat signals. Never for money, inventory, or anything with business consequences.

At-Least-Once (The Default)

The producer retries until it receives an acknowledgment. The consumer ACKs only after successful processing. If anything fails in between, the message is redelivered. The downside: the same message can arrive two or more times โ€” your consumer will eventually see a duplicate.

This is the right default for almost every production workload. It requires your consumers to be idempotent.

// โœ… Idempotent consumer โ€” INSERT-first pattern, safe under concurrent redelivery
async function processOrder(event: CheckoutEvent): Promise<void> {
  try {
    await db.transaction(async (tx) => {
      // INSERT the idempotency marker first โ€” throws unique constraint on duplicate
      // This is atomic: exactly one concurrent worker wins the insert, others throw
      await tx.processedEvents.insert({ orderId: event.orderId, processedAt: new Date() });
      await tx.orders.updateStatus(event.orderId, "confirmed");
    });
  } catch (err) {
    if (isUniqueConstraintViolation(err)) {
      // Another worker already processed this delivery โ€” ACK without reprocessing
      return;
    }
    throw err; // Real failures must NOT be ACKed โ€” redeliver for retry
  }
}
// Why not check first then insert? Two concurrent workers can both pass the check
// before either inserts (TOCTOU race), leading to double-processing. INSERT-first
// with constraint handling is the only race-condition-free pattern.

Idempotency is your responsibility โ€” the queue doesn't enforce it

At-least-once delivery guarantees your message arrives. It does not guarantee it arrives exactly once. Under network partitions or consumer restarts, the same message will appear multiple times. If your consumer charges a credit card, sends an email, or deducts inventory on every delivery, duplicates are catastrophic. Build idempotency at the consumer level using a processed-events table or a Redis SET of processed IDs โ€” before you deploy the consumer, not after the first incident.

Exactly-Once

Every message is processed exactly one time. In practice, true exactly-once requires either:

  • Transactional producers โ€” Kafka's transactional API, atomically committing a message and your DB write in a single two-phase commit.
  • Idempotency + deduplication โ€” At-least-once delivery combined with an idempotency key and a processed-events store. This is "effectively exactly-once" โ€” a practical equivalent, not a protocol guarantee.

Full exactly-once is expensive (coordination overhead) and usually overkill. The industry standard: idempotent consumers with at-least-once delivery. The result is effectively exactly-once semantics at a fraction of the operational complexity.

GuaranteeMessages lostDuplicatesComplexityUse case
At-most-oncePossibleNeverLowNon-critical telemetry, ephemeral metrics
At-least-onceNeverPossibleMediumMost production workloads with idempotent consumers
Exactly-onceNeverNeverHighFinancial transactions, inventory mutation, billing

Trade-offs

ProsCons
Fault isolation โ€” a consumer crashing doesn't affect the producer or any other consumerEventual consistency โ€” no single transaction boundary across producer and consumers
Traffic absorption โ€” the queue buffers bursts; consumers drain at their own sustainable rateOperational overhead โ€” the broker is a new stateful system to deploy, monitor, back up, and scale
Independent scaling โ€” add consumer instances to increase throughput without touching producersDebugging complexity โ€” tracing a message through an async pipeline requires correlation IDs and distributed tracing
Durability โ€” persisted messages survive consumer restarts and network partitionsLatency โ€” processing is non-realtime; the queue adds variable lag between publish and consumption
Decoupled deployments โ€” producers and consumers can be released independentlyIdempotency burden โ€” at-least-once delivery means consumers must handle duplicates; this logic is non-trivial for stateful operations
Natural backpressure โ€” queue depth signals overload before consumers crashMessage ordering โ€” guaranteed ordering requires special configuration (Kafka partitioning, SQS FIFO) and limits parallelism

The fundamental tension here is decoupling vs. observability. Synchronous calls are easy to trace โ€” request in, response out, error thrown. Async pipelines are significantly harder to reason about: I still remember the first time I had to debug a stuck consumer โ€” there was no request trace, just a rising queue depth metric and workers that all reported healthy. The resilience gain comes at the direct cost of end-to-end transparency.


When to Use It / When to Avoid It

Use message queues when:

  • Any downstream call takes >200ms and the caller doesn't need the result to return a response to the user.
  • You call a service that can be independently slow or unavailable โ€” any third-party API (email, SMS, payment webhooks).
  • Traffic spikes are unpredictable and the downstream cannot scale fast enough to absorb them elastically.
  • Multiple independent services all need to react to the same event โ€” fan-out without explicit tight coupling.
  • Background jobs need to be distributed across many worker instances for horizontal throughput.
  • You need guaranteed at-least-once processing โ€” fire-and-forget HTTP calls silently drop messages on network failure.

Avoid message queues (or know the full cost) when:

  • The caller needs the downstream result to return a response. A checkout cannot be "confirmed" to the user if inventory deduction is asynchronous โ€” you need to know the seat exists before printing a ticket.
  • You haven't built idempotent consumers. Queues will give you duplicates eventually โ€” this is not a matter of if, it's a matter of when.
  • You're prototyping. Queues add infra complexity that obscures bugs. Prove your system works synchronously first, then decouple proven bottlenecks.
  • Low-traffic, straightforward workloads where synchronous calls work fine and the downstream is reliable. Not every service interaction needs a queue.

So when does this actually matter in an interview? Every time the interviewer asks "what if the email service goes down?" or "how do you handle a notification service that's slower at peak?" โ€” that's your cue to draw a queue and explain the tradeoff.


Real-World Examples

Netflix โ€” Kafka as the spine of a microservices architecture

Netflix processes billions of events per day through Kafka: stream starts, encoding job completions, A/B experiment signals, payment events. Their video encoding pipeline alone uses Kafka to coordinate hundreds of worker types โ€” raw video ingest publishes events, encoding workers consume and transform, quality check workers verify, CDN distribution workers push final renditions. Each stage is decoupled: a bug in quality checking doesn't block video ingest. Netflix's defining Kafka insight: consumer group replay was critical for recovery. When a consumer bug corrupted data, they reset the consumer group offset, redeployed the fixed consumer, and reprocessed the entire event stream from the point before corruption โ€” recovering perfect state without manual data repair. Without replay semantics, that recovery would have required weeks of manual fixes.

Stripe โ€” idempotency as a first-class design principle

Stripe processes billions of dollars in transactions. Their public API accepts an Idempotency-Key header on every write, and their internal event consumers apply the same pattern. When Stripe charges a customer, the charge event is published at-least-once to downstream processors โ€” ledger updates, email confirmations, merchant webhook deliveries โ€” all of which are idempotent consumers. Stripe does not use exactly-once delivery. Their engineering posts are explicit: they use at-least-once delivery with idempotency keys stored in Redis (fast path deduplication) backed by PostgreSQL (durable idempotency record). Duplicate processing is safe to retry indefinitely without double-charging. The lesson: "exactly-once" is a complexity trap. Idempotent at-least-once gives you the same business guarantee at a fraction of the operational cost.

LinkedIn โ€” Kafka was built here for a reason

Kafka was created at LinkedIn in 2010 because no existing message queue met their requirements: streaming 1 trillion messages per day across activity logs, infrastructure metrics, real-time site analytics, and newsfeed data. Legacy brokers (ActiveMQ, RabbitMQ) couldn't sustain the throughput, and crucially couldn't replay events โ€” they deleted messages after consumption. LinkedIn's hard requirement was that a consumer reporting infrastructure could replay 7 days of event history to backfill a new data warehouse. The log-based model โ€” append-only, immutable, replayable โ€” was the architectural innovation that made Kafka different from everything before it. Today, every major data pipeline (Uber, Airbnb, Pinterest) runs on Kafka for the precise reason LinkedIn built it: you cannot afford to permanently lose the ability to reprocess your event history.


How This Shows Up in Interviews

When to bring it up proactively

Draw a queue the moment any downstream call is async, latency-sensitive, or unreliable. Say: "I'd decouple this with a message queue โ€” the API publishes an event and returns immediately. Downstream processing happens async with at-least-once delivery and idempotent consumers." The phrase "at-least-once delivery with idempotent consumers" signals you know what a queue actually costs, not just that it exists.

Don't just draw the box โ€” own its failure modes

Every interviewer accepts "add a queue here" as a starting answer. The senior/staff questions come immediately after: "What if the queue itself goes down?" "How do you prevent duplicate charge emails?" "What's your DLQ strategy?" "How do you know consumers are keeping up?" If you can't answer these in the same breath, the queue in your diagram works against you.

Depth expected at senior/staff level:

  • State your delivery guarantee and what it costs the consumer. "At-least-once, which means my email consumer needs idempotency โ€” I use an orderId check in a processed_events table before sending."
  • Name your visibility timeout and explain the redelivery bound. "30-second visibility timeout โ€” if the worker doesn't ACK in 30 seconds, SQS redelivers to another available instance."
  • Address the DLQ proactively. "After three retries, messages route to a DLQ and my PagerDuty fires. A message in the DLQ is always a code bug โ€” I never silently discard DLQ messages."
  • Know how to monitor consumer health. "I alert on queue depth exceeding 5 minutes of expected draining time. If depth grows faster than consumers drain, I scale consumer instances horizontally or investigate the downstream they're calling."
  • Distinguish fan-out patterns. "One checkout event fans out to N independent subscribers โ€” email, inventory, analytics each subscribe independently, fail independently, and retry independently."

Common follow-up questions and strong answers:

Interviewer asksStrong answer
"What if a consumer crashes mid-processing?""Visibility timeout expires without an ACK and the broker redelivers to another consumer. The consumer must be idempotent โ€” partial processing followed by redelivery must produce the same final state as a clean first-time delivery."
"What's the difference between a queue and Kafka?""SQS: competing-consumer task dispatch โ€” each message consumed once by one worker, no replay, fully managed. Kafka: distributed append-only log โ€” multiple independent consumer groups each consume from their own offset, replay is native, throughput is millions/sec. Different tools for different problems."
"How do you prevent sending the same email twice?""INSERT-first into a processed_events table with a unique constraint on orderId โ€” inside a transaction. A duplicate key violation means another worker already handled it; return early and ACK. INSERT-first is TOCTOU-safe: no check, no race, no duplicate send."
"How do you know consumers are keeping up?""Alert on queue_depth / consumer_throughput_rate > 5 minutes. Autoscale consumer pods horizontally when depth grows. If depth continues growing after scale-out, the problem is inside the consumer code โ€” usually a slow downstream call, not insufficient parallelism."
"What's a Dead Letter Queue and when do you use it?""Messages land in the DLQ after exceeding max_receive_count โ€” typically 3โ€“5 attempts. Every DLQ message is an operational signal: unhandled consumer code case or permanently broken downstream. I'd always alert on new DLQ depth, never silently discard."

Test Your Understanding


Quick Recap

  1. A message queue decouples producers from consumers using a persistent broker โ€” the producer publishes and returns immediately; the consumer processes asynchronously at its own pace, completely independent of the producer's HTTP response cycle.
  2. The broker persists messages until acknowledged โ€” a consumer crash before ACKing triggers redelivery after the visibility timeout expires, making at-least-once delivery the default guarantee and idempotent consumers a non-negotiable requirement.
  3. Point-to-point queues deliver each message to exactly one competing consumer (work dispatch); pub/sub topics fan-out independent copies to every subscriber (event broadcast) โ€” choose based on whether you need work distribution or event propagation.
  4. Kafka is a distributed append-only log built for high-throughput event streaming with replay; SQS is a fully managed simple queue for task dispatch โ€” they solve different problems and are not interchangeable defaults.
  5. The Dead Letter Queue is your safety net for messages that exceed maximum retries โ€” monitor DLQ depth as aggressively as regular queue depth, because a growing DLQ is always a code bug, not a traffic spike.
  6. Consumer group parallelism (SQS: worker instances; Kafka: partition count) is the lever for consumer throughput โ€” but it cannot fix a bottleneck in a slow downstream call your consumers are making.
  7. In every interview, name your delivery guarantee, visibility timeout rationale, DLQ strategy, and idempotency mechanism in the same 30-second explanation as the queue box itself โ€” that combination signals genuine operational understanding.

Related Concepts

  • Microservices โ€” Message queues are the most common mechanism for decoupling microservices in production. Understanding service boundaries helps calibrate when synchronous RPC vs. async queues is the right communication primitive for a given interaction.
  • Event sourcing โ€” Event sourcing treats every state change as an immutable event appended to a log โ€” a natural architectural companion to Kafka. Understanding event sourcing explains why Kafka's log-based retention model is fundamentally different from a traditional task queue.
  • Caching โ€” Caches and queues both protect downstream services: caches absorb read fan-out, queues absorb write fan-out and decouple failure domains. Knowing when to reach for each is one of the most practical system design distinctions.
  • Databases โ€” The Transactional Outbox Pattern โ€” writing events to a database outbox table atomically with your business data, then tailing that table into a queue โ€” is the canonical way to guarantee event delivery without distributed transactions across service boundaries.
  • Load balancing โ€” Competing consumer patterns in queues achieve the same horizontal throughput scaling that load balancers achieve for HTTP traffic โ€” both distribute work across homogeneous workers. Understanding both gives you the full picture of horizontal scaling across sync and async workloads.

Previous

CAP Theorem

Next

Rate limiting

Comments

On This Page

TL;DRThe Problem It SolvesWhat Is It?How It WorksKey ComponentsTypes / VariationsPoint-to-Point (Queue)Publish/Subscribe (Topic Fan-out)Push vs. Pull ConsumptionKafka vs. RabbitMQ vs. Amazon SQSDelivery GuaranteesAt-Most-OnceAt-Least-Once (The Default)Exactly-OnceTrade-offsWhen to Use It / When to Avoid ItReal-World ExamplesHow This Shows Up in InterviewsTest Your UnderstandingQuick RecapRelated Concepts