System Design Interview Questions: Dead Letter Queue Patterns
Reviewed by Mark Dickie · Last updated
9 questions on this page you can answer and see marked on the spot. Go to the first one
A dead letter queue (DLQ) is a separate queue where a message broker routes messages that cannot be processed after a configured number of delivery attempts. For a system design interview, you should know the retry-and-DLQ lifecycle: a consumer retries a failed message up to N times, then the broker moves it to the DLQ so the main queue stays unblocked. Interviewers want you to explain when a DLQ is preferable to in-place retry, how backoff and max-attempt settings interact, and what happens to poison messages that would otherwise block a queue indefinitely.
What should I know about dead letter queues for a system design interview?
A DLQ is one of the core reliability patterns in distributed messaging, and interviewers test whether you can place it correctly among retry, idempotency, and circuit-breaker strategies. You should be able to describe the lifecycle of a failed message, the role of exponential backoff, and the operational work of draining a DLQ once it fills.
| Concept | What it means | Why an interviewer asks |
|---|---|---|
| Max receive count | The number of delivery attempts before a message is moved to the DLQ | Tests whether you set a real bound instead of infinite retry |
| Redrive policy | The rules that move messages from a source queue to the DLQ | Checks you understand the broker-specific mechanism (SQS, RabbitMQ, Kafka) |
| Poison message | A message that always fails, often due to malformed content | Tests whether you isolate bad messages from the main processing path |
| Backoff strategy | Increasing the wait between retries (fixed or exponential) | Checks you avoid thundering-herd load on downstream dependencies |
- State the failure mode you are protecting against before proposing a DLQ — infinite retry, poison messages, or transient downstream outages.
- Set a max receive count that is high enough to ride out a short blip but low enough that poison messages do not stall a worker for minutes.
- Apply exponential backoff between retries so a downstream service outage does not get hammered with immediate re-deliveries.
- Decide whether DLQ messages get inspected by a human, auto-reprocessed after a fix, or discarded — and say which alert fires when the DLQ depth grows.
- Make the consumer idempotent, because retries and redrive both deliver the same message more than once.
How do DLQs differ across common message brokers?
In Amazon SQS a redrive policy moves messages to a configured DLQ after maxReceiveCount attempts. RabbitMQ uses dead-letter exchanges with x-dead-letter-exchange arguments, and the routing key can change on dead-lettering. Kafka does not have a native DLQ; the common pattern is for the consumer to catch the exception and publish the failed record to a separate error topic. Knowing these differences lets you explain the tradeoffs when an interviewer asks you to pick a broker for a given reliability requirement.
Key facts
- Tarmac's System Design interview questions cover 24 questions at difficulty 1–5 of 5.
- Tarmac tracked 3,763 job postings asking for System Design in September 2026.
- Roles asking for System Design advertise a median base salary of US$180,347, across 1,250 job postings as of September 2026.
- Tarmac last reviewed these System Design interview questions on 13 October 2026.
At a glance
| Questions | 24 |
|---|---|
| Difficulty | 1–5 of 5 |
| Formats | True / false, Short answer, Flashcard, Multiple choice, Find the bug, Design exercise |
What you'll review
- dead letter queue
- message queues
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
True or false: In messaging systems, a dead-letter queue is a queue to which messages that exceed the configured maximum number of processing retry attempts are automatically routed for later inspection or manual re-processing.#
Options
Show answer
True. A dead-letter queue (DLQ) is a secondary queue that receives messages once they exceed the configured maximum number of processing retries. This keeps poison messages out of the main queue so they can be inspected or reprocessed manually.
A dead-letter queue is defined by its routing behavior: when a consumer fails to process a message within the allowed retry count, the broker or consumer framework moves that message out of the main queue into the DLQ. There it is kept for later analysis, debugging, or manual re-processing, so it stops blocking or repeatedly failing the main flow. The statement describes exactly this defining behavior, so it is true.
In a message-based system using a dead-letter queue, what is the primary role of the dead-letter queue?#
Show answer
A dead-letter queue holds messages that failed processing after exhausting the configured retry attempts, so they can be inspected and possibly reprocessed without blocking the main queue.
A dead-letter queue is a dedicated queue that receives messages that failed processing after the system's configured retry attempts are exhausted. It keeps these problematic messages out of the main pipeline so they can be inspected, debugged, and optionally reprocessed, rather than repeatedly blocking or poisoning the active queue.
What is a dead-letter queue (DLQ)?#
Show answer
A dead-letter queue is a dedicated queue that receives messages which could not be successfully processed after a configured number of retry attempts. It lets operators inspect, debug, and reprocess those failed messages without blocking the main processing pipeline.
A dead-letter queue (DLQ) is a specialized queue that captures messages which failed processing after exhausting configured retries. It separates unprocessable messages from the main queue, which enables debugging and reprocessing without blocking normal message flow. The question asks for a definition in the reader's own words, so it is an open answer graded by keywords rather than a choice or blank.
What is a dead-letter queue (DLQ) and what problem does it solve in a message-based system?#
Show answer
A dead-letter queue is a secondary queue where messages that cannot be processed successfully — typically after exceeding a configured retry or receive-count limit — are automatically routed so they can be inspected, debugged, and optionally replayed later, instead of being retried indefinitely or dropped silently.
A DLQ isolates problematic messages so they do not block the main queue or cause infinite retry loops. It gives operators a safe place to inspect failures (poison-pill messages, bad payloads, downstream outages) and replay them once the root cause is fixed, while normal traffic continues flowing through the primary queue.
In a typical message-queue system (e.g., AWS SQS with a redrive policy), which scenario causes a message to be moved to a dead-letter queue?#
Options
Show answer
A message is moved to a dead-letter queue when it exceeds the configured maximum receive count or retry limit without being successfully acknowledged by a consumer. Successful processing, new subscriber connections, and initial queue creation never trigger dead-letter routing.
A dead-letter queue receives messages that have failed processing repeatedly. When a message's receive count exceeds the configured maximum (e.g., SQS maxReceiveCount) without a successful ack/Delete, the broker routes it to the DLQ. Successful acknowledgements remove the message from the main queue entirely; new consumer subscriptions and initial queue creation are unrelated lifecycle events that never trigger DLQ routing.
Which statement correctly describes the role of a Dead Letter Queue (DLQ) in a message-oriented architecture?#
Options
Show answer
A Dead Letter Queue is a secondary queue that receives messages after they exhaust their configured retry attempts on the main queue. It isolates poison-pill messages so they can be inspected or reprocessed without blocking healthy traffic on the main queue.
A Dead Letter Queue isolates messages that fail processing after exhausting configured retries, so they can be inspected and reprocessed later without stalling the main queue. Holding messages for inactive subscribers, replicating the queue for disaster recovery, and restoring FIFO order are separate concerns handled by different mechanisms, so the option describing a secondary queue for retry-exhausted messages is the only correct one.
An order-processing service consumes messages from a queue. Occasionally a message contains malformed JSON that causes the consumer to throw an exception every time it attempts to process it. Because the queue's redelivery logic keeps re-delivering the same message, subsequent healthy messages are starved and throughput drops to near zero.<br><br>Which pattern most directly prevents a single unprocessable message from blocking the queue?#
Options
Show answer
Move messages that exceed a maximum retry count to a dead-letter queue (DLQ) so they no longer block the main queue. A 'poison pill' message — one that always fails, such as malformed JSON — would otherwise be redelivered indefinitely and stall processing. The DLQ isolates such messages for later inspection or manual reprocessing, while healthy messages continue flowing through the main queue.
A dead-letter queue (DLQ) is a secondary queue that receives messages the consumer failed to process after a configured retry limit. When a 'poison pill' message — one that always fails, such as malformed JSON — keeps being redelivered, the DLQ pattern diverts it out of the main queue so it no longer blocks healthy messages. The failed message is preserved for inspection, debugging, or manual reprocessing once the root cause is fixed. Increasing concurrency does not solve the problem because the bad message will still be redelivered and fail repeatedly; it merely delays starvation. Shortening retention silently discards data without giving operators a chance to diagnose the failure. Changing the delivery model (pull vs. push) is orthogonal — a push-based consumer still receives the same poison-pill message and will still fail on it.
A message consumer crashes with a deserialization error every time it processes a particular message, causing the broker to keep redelivering it — consuming CPU indefinitely. Which mechanism is specifically designed to break this loop?#
Options
Show answer
A dead-letter queue (DLQ) breaks the loop: after a configured number of delivery attempts the broker routes the failing message to the DLQ for inspection, freeing the main queue to keep processing healthy messages. Engineers can then diagnose the bad payload at their own pace. Switching to at-most-once or a short TTL silently drops the message, and adding consumers just spreads the same deserialization crash to more nodes.
A dead-letter queue is the correct tool: after a configurable retry ceiling (e.g. 5 delivery attempts) the broker automatically moves the problematic message to a separate DLQ so it can be inspected, replayed, or discarded without blocking the main queue. The consumer keeps processing the rest of the stream while engineers diagnose the bad payload at their own pace. Switching to at-most-once silently drops messages on failure — you lose the event permanently without knowing why it failed. A short TTL is a blunt forced expiry that also loses the message with no diagnostic value. More consumers just propagate the crash to more instances — every consumer hits the same deserialization error on the same payload, amplifying the problem rather than containing it.
In a dead-letter queue (DLQ) pattern for an at-least-once delivery system, a consumer reads messages from a Kafka topic. The system applies retry-with-exponential-backoff before moving a message to the DLQ. Which scenario should route a message to the DLQ immediately, with zero retries?#
Options
Show answer
A message whose payload fails JSON schema validation during deserialization should go straight to the dead-letter queue with zero retries. The defect is in the message content, so retrying the same payload can never succeed. Timeouts, broker disconnects, and temporary database outages are transient and should be retried with backoff first.
A payload that fails JSON schema validation has a permanent, content-level defect: the message itself is structurally invalid, so retrying the same bytes will fail every time. Routing it straight to the DLQ avoids wasting retry budget and downstream capacity. The other three scenarios are transient infrastructure failures: a downstream timeout, a dropped broker connection, and a temporarily unavailable database may all succeed on a later attempt, so they should be retried with exponential backoff before any DLQ routing is considered.
Design a dead-letter queue (DLQ) subsystem for an order-processing microservice architecture. The main pipeline consumes events from a Kafka topic, processes payments, updates inventory, and sends notifications. Messages can fail due to transient issues (downstream timeouts, rate limits, brief database unavailability) or permanent issues (schema violations, invalid references, poison-pill payloads that crash the consumer). Your design must address: (1) how failures are classified and routed, (2) the retry policy including backoff and the threshold for DLQ placement, (3) how DLQ messages are inspected and re-injected into the pipeline, and (4) how the system prevents unbounded reprocessing loops. Provide your architecture, key components, and any trade-offs.#
Show answer
Classification: the consumer maps each exception to a category. Transient errors (timeouts, rate limits, connection drops) are retried; permanent errors (schema violations, unknown entity references, unparseable or poison-pill payloads) skip retries and go straight to the DLQ topic. Retry policy: transient failures use exponential backoff with jitter (for example base 100ms doubled per attempt, capped at 60s) for a bounded number of attempts, such as 5; when the budget is exhausted the message is moved to the DLQ, a separate Kafka topic with longer retention. Each DLQ record keeps the original payload plus metadata: failure class, error message, stack trace, retry history, source topic, and original offset. Inspection and re-injection: a DLQ dashboard shows depth, message age, and failure-type breakdown with alerts on thresholds; an ops tool displays payload and metadata, and after the root cause is fixed a replay endpoint re-publishes the message to the main topic with its retry count reset. Loop prevention: each message carries a requeue count incremented on every re-injection; past a threshold (for example 3) it is parked in a manual-review queue that needs human approval before another retry. Trade-offs: backoff adds latency for messages that eventually succeed but protects downstream services from retry storms; the requeue cap stops automation from masking persistent bugs but can delay fixable messages until a human reviews them.
A robust DLQ design separates transient from permanent failures so that retriable errors get bounded exponential backoff with jitter while permanent errors go straight to a dead-letter store. Messages that exhaust the retry budget land in a separate topic with enriched metadata, and tooling lets operators inspect and replay them after the root cause is fixed. Loops are prevented by tracking a requeue count per message and gating further retries behind a cap or a manual-review step, which bounds reprocessing while accepting some latency and human effort as the trade-off.
In a message-processing system that uses a dead-letter queue, which failure category should be retried with exponential backoff before the message is sent to the DLQ, rather than being routed to the DLQ on the first failure?#
Options
Show answer
Transient failures, such as a downstream service timeout, a brief database outage, or a rate-limit response, should be retried with exponential backoff before a message reaches the dead-letter queue. The underlying condition often clears on its own, so a retry can succeed. Permanent failures such as schema violations or poison-pill payloads will fail every time and belong in the DLQ right away.
The DLQ routing decision depends on whether a failure is transient or permanent. Transient failures, such as a downstream timeout, a brief database outage, or a rate-limit response, can succeed on a later attempt, so they warrant retries with exponential backoff before DLQ placement. The other three choices, a schema-validation failure, a poison-pill message that deterministically crashes the consumer, and an invalid foreign-key reference, are permanent: retrying the same message reproduces the same failure, so they should go to the DLQ immediately to avoid wasting capacity.
In an asynchronous message-processing system that uses a dead-letter queue (DLQ), the consumer reads messages from the primary queue and attempts to process each one. Under which condition should the message be routed to the DLQ rather than retried on the primary queue?#
Show answer
A message should be routed to the DLQ when it has exhausted all configured retries, meaning transient failures keep recurring past the retry limit, or when the failure is non-transient, such as a malformed payload or schema violation that no retry could fix. A transient failure with retries remaining, like an HTTP 503 or 429, should be retried with backoff on the primary queue instead.
A dead-letter queue holds messages that cannot be processed successfully, either because all configured retries have been used up on failures that kept recurring, or because the failure is non-transient (for example a malformed payload or schema violation) so retrying would never help. A transient failure with retries remaining, such as an HTTP 503 or a 429 Too Many Requests, should be retried with exponential backoff rather than dead-lettered. Consumer overload is a load-shedding concern handled by backpressure or rate limiting, not by moving individual messages to the DLQ.
You are designing the message-processing pipeline for an e-commerce platform. Orders are published to a message queue and consumed by worker services that call downstream APIs (payment, inventory, shipping). Under load, some messages fail repeatedly, either because a downstream service is temporarily unavailable or because the message itself is malformed. Propose a dead-letter queue (DLQ) architecture for this pipeline. Your design should cover: (1) the queue topology and routing mechanism that moves a failed message from the main queue to the DLQ, (2) the retry policy that decides when a message is 'dead' rather than retried, (3) how the DLQ is monitored and reprocessed, and (4) how you distinguish transient failures from permanent (poison-pill) failures.#
Show answer
Orders arrive on a primary orders.processing queue. (1) Topology and routing: a separate orders.dlq queue is attached to the main queue through a dead-letter mechanism. In RabbitMQ this is a dead-letter exchange (DLX) bound to orders.dlq, with the main queue configured to dead-letter rejected messages (for example a quorum queue with x-dead-letter-exchange and a x-delivery-limit). In AWS SQS it is a redrive policy on the main queue that names the DLQ ARN and a maxReceiveCount (e.g., 5); once a message has been received that many times without deletion, SQS moves it to the DLQ automatically. (2) Retry policy: transient failures are retried with exponential backoff (1s, 2s, 4s, 8s, 16s) plus jitter, up to a maximum of 5 delivery attempts. After the final failed attempt the message is dead-lettered rather than retried again, which prevents infinite retry loops and thundering-herd load on recovering downstream services. (3) Monitoring and reprocessing: a CloudWatch alarm or Prometheus alert fires when DLQ depth exceeds a threshold (e.g., 10 messages) and pages the on-call engineer. Dead-lettered messages keep their original payload and metadata, including failure reason and attempt count. After the root cause is fixed, a replay tool or dedicated DLQ consumer moves messages back to the primary queue. (4) Transient vs permanent: workers classify errors. Timeouts, 5xx responses and 429 rate-limit responses are transient and consume retry budget. Schema validation failures, missing required fields and 4xx responses that indicate a bad message are permanent poison pills and are dead-lettered immediately without consuming retry budget. A pre-processing validation step rejects structurally invalid messages before they enter the retry loop at all.
A complete answer must address four pillars: a physically separate dead-letter destination with a concrete routing mechanism (SQS redrive policy with maxReceiveCount, or a RabbitMQ dead-letter exchange binding, or an explicit nack without requeue); a bounded retry policy with exponential backoff and jitter so messages do not loop forever; operational monitoring through depth alerts plus a replay path once the root cause is fixed; and a classification scheme that separates transient downstream errors (timeouts, 5xx, 429) that consume retry budget from permanent poison-pill errors (schema or 4xx bad-message failures) that are dead-lettered immediately, backed by pre-validation of structurally invalid payloads. The sample answer covers all four with broker-specific detail, so it serves as a mid-level reference that a grader can check against the keywords and the four pillars.
What is a dead-letter queue (DLQ) and when does a message end up there?#
Show answer
A DLQ is a side queue where a message broker parks messages that cannot be successfully processed after exhausting retries. A message lands in the DLQ when: it has been delivered and negatively acknowledged (nack) more times than the configured maxReceiveCount, its TTL expires before a consumer picks it up, or the message is malformed and rejected by consumer validation. DLQs prevent poison messages from blocking the main queue indefinitely, allow operators to inspect failures without data loss, and enable replaying fixed messages back to the main queue after the root cause is resolved.
Without a DLQ, a single unprocessable 'poison' message cycles forever, blocking queue progress or consuming retries that could serve real work. DLQs make failure observable and recoverable. The companion alarm pattern is to alert when DLQ depth rises above zero, treating any DLQ message as a bug requiring investigation.
This worker pulls messages off a queue and processes them. A single malformed message has brought the whole consumer to a halt — no other message gets processed. What is the bug?#
// queue.receive() blocks until a message is available
while (true) {
const msg = queue.receive();
try {
process(msg); // throws on a malformed message
queue.ack(msg);
} catch (err) {
queue.requeue(msg); // put it back so we don't lose it
}
}Options
Show answer
A message that always throws (a 'poison pill') is requeued endlessly with no attempt limit, so it loops forever at the head of the queue and starves every other message; it needs a redelivery counter that routes to a dead-letter queue after N attempts
The message is a poison pill: it deterministically throws, the catch block requeues it with no attempt limit, and (in a FIFO queue) it returns to the head and is retried forever — blocking every message behind it. The fix is to bound redeliveries (track an attempt count or use the broker's max-receive setting) and route the message to a dead-letter queue after the threshold, so the poison message is quarantined for inspection while the rest of the stream flows. The distractors are wrong: ack-after-process is the correct at-least-once ordering, not the bug — acking first would silently lose messages on a crash; the infinite while loop is the intended consume loop, not a memory leak; and changing requeue to ack on failure would discard every failed message, replacing a stall with silent data loss.
Design a multi-channel notification system: a platform service that other services call to send notifications to users over push, email, SMS, and in-app.#
Show answer
Requirements. A single send API fans to four channels, each fronted by a third-party provider with its own rate limit and failure profile. Two priority tiers: transactional (OTP, < 5 s) and marketing (batchable). First-class requirements beyond delivery: idempotency (never double-send), preferences/opt-out, retry, and an audit trail.
Pipeline. The send API does almost nothing synchronously — it validates and enqueues the event onto a message queue, then returns. Per-channel workers consume from their queue and call the provider. This decoupling is the backbone: it absorbs the 30K/sec peaks, and a slow provider only backs up its queue, not the others. Templating (render the message body from a template + data) happens in the worker just before send.
Dedup & idempotency. Each event carries an idempotency key (event_id:user_id:channel). Before sending, the worker does an atomic check-and-set against an idempotency store; if the key already exists, it's a duplicate and is dropped. This turns the queue's at-least-once delivery into at-most-once per recipient per channel, so an upstream retry can't double-send.
Reliability. Transient provider errors are retried with exponential backoff + jitter. A message that exhausts its retries (or is malformed/poison) goes to a dead-letter queue for inspection rather than blocking the worker. Because each channel has its own queue and worker pool, a failing SMS provider can't stall push or email.
Preferences. Before dispatch, the worker checks the user's preferences/opt-out for that channel and any per-user frequency cap (so we don't spam someone with ten pushes in a minute), plus compliance rules (quiet hours, unsubscribe for email). A suppressed notification is recorded as suppressed in the audit, not sent.
Scaling & provider limits. Each channel scales its worker pool independently. Workers pace themselves with a token bucket matched to the provider's rate limit, applying backpressure to the queue rather than overrunning the provider. Priority queues (a separate high-priority lane per channel) let an OTP jump ahead of a 50M-message marketing backlog so it still meets its < 5 s budget.
A notification system is fundamentally an asynchronous, queue-backed pipeline problem. The defining move is decoupling ingestion from delivery: the send API just enqueues, and per-channel workers deliver — which is what absorbs traffic spikes and stops one slow third-party provider from stalling the others. The two correctness pillars are idempotency (an idempotency key like event:user:channel checked before send turns the queue's at-least-once delivery into at-most-once per recipient, so upstream retries never double-send) and reliability (backoff retries plus a dead-letter queue for poison messages). Preferences/opt-out and per-user frequency caps must be enforced before dispatch, and priority queues are what let a latency-critical OTP overtake a bulk marketing backlog.
Design a distributed task / job queue (think Celery / Sidekiq / SQS-backed workers): producers submit background jobs, a fleet of workers executes them, and the system must not lose work or run a job twice in a way that corrupts state.#
Show answer
Requirements. The system has two halves: submit (cheap, synchronous — durably record the job and return) and execute (asynchronous, on a worker). Jobs vary wildly in duration (10 ms to minutes) and need scheduling and priority. The crucial semantic is delivery: I'll deliver at-least-once and make handlers idempotent, which gives exactly-once effect even for charge-card — true exactly-once delivery across a crash boundary is not achievable, so I don't promise it.
Queue & leasing. Jobs go into a durable, replicated broker (Kafka, SQS, or a Redis stream). A worker doesn't delete on dequeue — it leases the job with a visibility timeout (or in-flight/ack semantics): the job is hidden from other workers while being processed and is only acked (removed) on success. If the worker crashes, the lease expires, the job reappears, and another worker runs it. This is what guarantees no lost jobs on crash, at the cost of possible re-delivery.
Delivery semantics & idempotency. Because re-delivery is possible, every side-effecting handler is idempotent, keyed by a job/idempotency id. For charge-card: before charging, the worker does an atomic check-and-set on idempotency_key in a store; if it's already marked done, it skips the charge and just re-acks. So the second worker that picks up the re-delivered job sees the key, does nothing, and acks — the card is charged exactly once even though the job was delivered twice.
Retry, DLQ, scheduling, priority. Transient failures retry with exponential backoff + jitter (bounded attempts). A job that exhausts retries — or is malformed/poison — is moved to a dead-letter queue for inspection rather than looping forever. Scheduled jobs go to a delay queue or a timestamp-indexed store that a promoter moves into the ready queue when due. Priority is separate lanes (high/default/low) or a priority field, so urgent jobs jump ahead.
Scaling the queue & fleet. The broker is partitioned (by job type or key hash); throughput scales with partition count and worker count. The fleet autoscales on queue depth / consumer lag. Critically, long-running jobs get their own queues and worker pools, isolated from latency-sensitive ones — so a flood of 5-minute transcodes drains its own pool and never sits in front of send-email.
Failure modes. Broker down: it's a replicated durable log, so it tolerates node loss; producers buffer briefly. Worker crash mid-job: lease expiry → redelivery (idempotency makes that safe). Poison job: capped at N attempts then DLQ, so it can't crash workers forever. Head-of-line blocking: per-type queues plus priority lanes keep one backlog from starving the rest.
A distributed job queue is the canonical at-least-once + idempotency system. The defining move is leasing instead of delete-on-dequeue: a worker hides a job with a visibility timeout and only acks on success, so a crash mid-job makes the job reappear and re-run rather than vanish — which is exactly why no work is lost. The price of that guarantee is possible re-delivery, so every side-effecting handler must be idempotent (an idempotency key checked before the side effect), which is what turns at-least-once delivery into exactly-once effect even for a charge-card. The supporting machinery is backoff retries with a dead-letter queue for poison jobs (so one malformed job can't loop forever), scheduled/priority lanes, and — the most-missed point — isolating long-running jobs in their own pools so a transcode backlog never blocks fast jobs (head-of-line blocking).
A high-throughput e-commerce platform processes ~50,000 order events per second through a Kafka topic consumed by a microservice that persists orders to a database and triggers downstream fulfillment. Occasionally a message cannot be processed because of a schema incompatibility, invalid payload data, or a downstream dependency outage that times out. The engineering team needs a dead-letter queue (DLQ) strategy so that unprocessable messages are quarantined, inspected, and replayed without losing orders and without blocking the main pipeline. Design the DLQ architecture end-to-end. Specifically address: (1) the exact policy that decides when a message is routed to the DLQ versus retried, (2) the structure of a DLQ message including the metadata it must carry, (3) the replay workflow and the idempotency requirements it imposes on consumers, (4) the observability and alerting around the DLQ, and (5) the retention and lifecycle policy. Assume the consumer runs in a Kafka consumer group.#
Show answer
DLQ routing policy: Every message carries a per-message delivery-attempt counter persisted in a lightweight state store keyed by the Kafka partition + offset (or a dedup id for non-Kafka sources). On each failed processing attempt the consumer applies exponential backoff for transient failures (network timeouts, downstream 5xx) up to a configurable max.retries threshold, e.g., 3 retries with backoff 1s, 4s, 16s. If the failure is classified as non-retryable (schema mismatch, deserialization error, validation failure) it is routed to the DLQ immediately on the first attempt. When the attempt counter exceeds max.retries for a transient failure, the message is published to the DLQ topic instead of being retried again. The consumer commits its offset only after the message is either successfully processed or safely written to the DLQ, so at-least-once semantics are preserved without blocking the pipeline on a single poison message. The DLQ is a separate Kafka topic (orders.dlq) partitioned identically to the source topic so replay preserves ordering within a partition. DLQ message structure: Each DLQ record wraps the original payload and appends a metadata envelope: originalTopic (orders), originalPartition, originalOffset, originalTimestamp, messageId / correlationId, attemptCount, firstFailureTimestamp, lastFailureTimestamp, failureType (transient / non-retryable), errorCode, errorMessage, stackTrace (truncated), consumerVersion, and headers from the source record. This metadata is essential for triage, filtering by error type, and deterministic replay back to the exact source partition. Replay workflow: A separate replay consumer (or an operator-triggered job) reads from the DLQ topic. After the root cause is fixed (schema migration, downstream service restored, code patch deployed), the replay tool extracts the original payload and republishes it to the source topic orders, preserving the messageId/correlationId as a dedup key. Because replay re-delivers messages that may have already been partially processed (database write succeeded but acknowledgment failed, or a previous replay attempt), every downstream consumer must be idempotent: database writes use upsert-on-key (INSERT ... ON CONFLICT (order_id) DO UPDATE), external API calls carry an idempotency key header, and the fulfillment service deduplicates on messageId within a sliding window. Without idempotent consumers, replay would duplicate orders or double-charge customers. Observability and alerting: The system exports metrics for DLQ depth (dlq.messages.count), DLQ age (dlq.oldest.message.age.ms), DLQ ingress rate (dlq.messages.per.second), and DLQ error breakdown by failureType and errorCode. Alerts fire when DLQ depth exceeds a threshold (e.g., > 1,000 messages), when DLQ growth rate exceeds the normal baseline (e.g., > 10 messages/sec sustained for 5 minutes, which indicates a systemic issue rather than occasional poison messages), or when the oldest DLQ message exceeds a max age (e.g., > 1 hour, meaning the team is falling behind on triage). A dashboard ties DLQ ingress rate to the upstream consumer's per-error-code failure rates so the on-call engineer can immediately see which upstream change caused the spike. Retention and lifecycle: The DLQ topic is configured with retention.ms = 7 days (configurable) and a max size cap so it does not grow unbounded. Before messages expire, an archival job (e.g., a Kafka Connect S3 sink) copies DLQ records to cold storage (S3 with lifecycle rules) for compliance and post-mortem analysis. Capacity planning: at 50,000 msgs/sec with a 0.01% poison rate, the DLQ receives ~5 msgs/sec (~432K/day). A 7-day retention at that rate is ~3M messages; the DLQ topic is provisioned with headroom for burst scenarios (downstream outage) at 5x baseline, i.e., ~15M messages / 7 days, and the archival sink is scaled to keep up with peak ingress so the DLQ never silently drops messages.
A strong answer covers five load-bearing parts. (1) The routing policy must distinguish transient failures (retried with exponential backoff up to a max-retries threshold) from non-retryable failures (sent to the DLQ immediately), and the consumer must commit offsets only after DLQ write so poison messages never block the pipeline. (2) A DLQ record must carry the original payload plus metadata (original topic, partition, offset, timestamps, attempt count, error type and message, correlation id) for triage and deterministic replay. (3) Replay reads from the DLQ, republishes to the source topic, and requires idempotent consumers (dedup keys, upserts, idempotency headers) so reprocessing does not duplicate side effects. (4) Observability must track DLQ depth, oldest-message age, and ingress rate with concrete alert thresholds and a dashboard linking DLQ ingress to upstream error codes. (5) Retention must be bounded (TTL plus max-size), with archival to cold storage before deletion and capacity planning scaled for burst scenarios.
A microservice consumes messages from an Amazon SQS standard queue. The queue has a redrive policy that moves messages to a dead-letter queue (DLQ) after 5 receive attempts. The service uses at-least-once processing and occasionally crashes after completing its business logic but before deleting the message from SQS. After a production bug is fixed, you plan to replay all messages from the DLQ back through the original queue. Which statement is correct?#
Options
Show answer
Replaying an SQS dead-letter queue requires an idempotent consumer. SQS dead-letters messages based on receive count, not on whether processing succeeded, so a message whose work finished before a crash can end up in the DLQ. Replaying it without idempotency would repeat side effects such as duplicate charges or records.
SQS moves a message to the DLQ based only on how many times it has been received (ReceiveCount) reaching the redrive threshold, not on whether processing succeeded. A consumer can complete the business logic and then crash before deleting the message, so the message returns to the queue, gets received again, and is eventually dead-lettered even though its work was already done. Replaying such messages through a non-idempotent consumer would repeat those side effects, so the operation must be idempotent. A longer visibility timeout does not remove the crash-after-processing case, and DLQ messages are ones that were received multiple times, so they are not unprocessed.
Design a dead-letter queue (DLQ) strategy for an event-driven payment processing system. Payment events arrive on a Kafka topic and flow through a chain of microservices (fraud check, ledger entry, notification), each consuming from its own topic and publishing to the next stage's topic. The system must handle poison-pill messages (messages that will always fail, such as unparseable payloads or unsupported currencies) without blocking the pipeline or losing data. Your design should cover: (1) the routing criteria that decide when a message lands in the DLQ, (2) the retry strategy preceding DLQ routing, (3) the DLQ message schema and metadata, (4) alerting and observability for DLQ health, and (5) the replay and recovery procedure for dead-lettered messages.#
Show answer
Each stage consumes from its input Kafka topic, processes the event, and publishes to the next stage's topic. Each consumer has a per-stage dead-letter topic. Routing criteria: a message goes to the DLQ either after exhausting a bounded retry budget (default 5 retries) or immediately on a terminal, non-retryable error such as schema validation failure, unparseable payload, or a business-rule rejection like an unsupported currency. Transient errors (downstream timeouts, rate-limit responses) are retried, so poison pills never occupy consumer capacity indefinitely. Retry: retryable failures use exponential backoff with jitter (up to about 20%) to avoid thundering-herd effects, bounded by a maximum retry count and a maximum total time window. In Kafka this is implemented with a retry-topic pattern using per-attempt delay topics or a delay-aware scheduler. Unbounded retries would let one poison pill block a consumer or fill the retry topic, creating back-pressure across the pipeline. DLQ schema: each DLQ record wraps the original payload in an envelope with original topic, partition, offset, the failing stage, exception type and message, truncated stack trace, first-failure and last-attempt timestamps, retry count, a schema version, and a correlation ID equal to the original payment event ID. This supports both automated replay and human forensics. Alerting and observability: emit metrics for DLQ depth, DLQ write rate, error-type histograms by exception and failure reason, and age of the oldest DLQ message. Alert when depth crosses a threshold (for example more than 100 messages in 5 minutes) or when the write rate jumps above baseline, which often signals a deployment regression. Grouping by error type distinguishes one poison pill (one message, one reason) from a systemic bug (many messages, same reason), and dashboards overlay deployment markers. Replay and recovery: a dedicated replay service re-injects messages into the pipeline entry point or the failed stage, keyed by correlation ID, after an operator or automated policy approves. Every stage is idempotent, using keys such as payment ID plus stage name, so a partially completed message does not double-write ledger entries or send duplicate notifications. Messages that keep failing after N replay attempts move to a quarantine state for manual review. Schema evolution is handled by versioned envelopes: the replay service applies migration shims to upgrade old payloads to the current contract, or flags messages whose version cannot be migrated for manual triage.
This is an open design question that asks the candidate to construct a complete DLQ strategy, so it is correctly a SHORT_ANSWER graded against the sample answer and keywords rather than a closed-choice format. It tests whether the candidate can separate transient from terminal errors, bound retries with backoff and jitter, design a metadata-rich DLQ envelope, instrument DLQ health, and perform idempotent replay with schema migration. These are senior-level concerns, which matches difficulty 4.
You are the architect for a large-scale, event-driven order pipeline at an e-commerce company. A single Kafka topic (orders) is consumed by four downstream services with very different characteristics: a payment authorization service (synchronous upstream dependency on a 3rd-party PSP, strict 2-second SLA, must-not-drop), an inventory reservation service (internal gRPC, retryable on transient stock-lock contention), a notification dispatch service (best-effort, tolerates eventual delivery), and a real-time analytics ingestion service (must never block — drops are acceptable, but needs audit). Today every consumer naively retries indefinitely or N times then drops to the floor; operational has no visibility into failures and engineering is blind to silently-dropped messages until customer complaints arrive. Your CTO asks you to design a comprehensive dead-letter queue (DLQ) strategy spanning all four consumers — addressing retry semantics, poison-pill isolation, replay/reprocessing, observability, and cascading-failure safety. In your answer, cover the per-consumer differences, the DLQ topology (shared vs. per-consumer), the message envelope/metadata needed for replay, the alerting and SLO model, and what happens if the DLQ itself becomes a bottleneck.#
Show answer
Topology: Per-Consumer DLQs, Not a Shared OneEach consumer has a different failure contract, so each gets its own dedicated DLQ topic (dlq.payments, dlq.inventory, dlq.notifications, dlq.analytics). A single shared DLQ would conflate error types with vastly different urgency (a dropped payment is P1; a dropped analytics event is P3), force a single retention policy, and create a noisy-neighbor risk where one misbehaving consumer floods the queue and crowds out others. Per-consumer DLQs let each service own its failure domain, set its own retention and alert thresholds, and evolve its replay tooling independently.## Retry Semantics: Bounded Exponential Backoff with Jitter, Then DLQEvery consumer wraps processing in a retry loop with exponential backoff + jitter to avoid thundering-herd retries against a recovering dependency. Critically, I distinguish transient vs. permanent failures at the application level using typed error classification — a transient failure (network timeout, 503, lock contention) is retried; a permanent failure (schema mismatch, invalid enum, referential integrity violation) is sent directly to the DLQ without consuming retry budget. Retry counts are bounded and consumer-specific: payment 3 retries (sensitive to PSP rate limits), inventory 5 retries (lock contention is typically short-lived), notifications 2 retries, analytics 1 retry (fast-fail, accept drops). The retry counter and original timestamp are carried in the message envelope — not just in a consumer-local map — so state survives consumer restarts and horizontal scaling. After exhausting retries (or on a classified permanent error), the message is published to that consumer's DLQ.## Poison-Pill IsolationA poison-pill message is one that always fails — typically because of unfixable payload corruption or an unhandled edge case in deserialization. I detect poison pills at two levels: (a) a per-message max-attempt counter in the envelope that triggers DLQ routing on the first permanent classification or after N retries, and (b) a circuit breaker on the consuming service that opens when the failure rate over a sliding window exceeds a threshold (e.g., >50% of last 1000 messages fail within 30 seconds), halting pulls to avoid burning CPU on a poison-pill storm. The circuit breaker half-opens after a cooldown to probe recovery. Poison-pill messages in the DLQ are tagged with failure_class and failure_reason so operators can triage: a burst of DESERIALIZATION_ERROR points to a bad producer; a burst of PSP_TIMEOUT points to an upstream incident.## Replay / Reprocessing PipelineEach DLQ message carries a replay envelope: original topic, original partition, original offset, original timestamp, retry count, failure class, failure reason, and the raw original payload (unmodified). Replay is not a simple re-produce to the original topic — that would lose ordering and risk duplicates. Instead, the replay service is a dedicated consumer of the DLQ topic that: (1) reads messages, (2) optionally applies a fix-up transformation (e.g., schema migration, field defaulting) if the failure was a known regression, (3) checks an idempotency key (derived from order ID + a dedup window) to avoid duplicate side effects, and (4) re-produces to the original topic with idempotent-producer enabled where the broker supports it. For the payment service specifically, the replay path must not re-charge — it must reconcile against PSP state first (query the PSP for the authorization; only replay if it never happened).## Observability & SLOsEach DLQ emits metrics: DLQ depth (lag), DLQ message age (time since original produce), DLQ produce rate, failure rate by failure_class, and replay throughput / success rate. SLOs are per-consumer and reflect business impact: payments SLO = DLQ depth < 10 messages or max age < 5 minutes (P1 alert); notifications SLO = depth < 1000 or age < 1 hour (P3); analytics = no DLQ at all — the service intentionally drops and increments a counter, with a P4 alert if drop rate exceeds 1% over 5 minutes. Dashboards break failures down by failure_class so engineering sees at a glance whether the problem is upstream (bad events), downstream (dependency outage), or code (unhandled errors).## Cascading-Failure Safety: DLQ Depth Limits and BackpressureThe DLQ itself can become a bottleneck — if a downstream replay service is down, the DLQ fills up and eventually hits Kafka retention limits, silently losing data. Mitigations: (a) DLQ retention policies sized to the consumer's acceptable replay window (payments: 30 days, analytics: 7 days), (b) a DLQ depth alarm that pages when depth exceeds a capacity threshold (computed from retention × partition count × max message size), (c) backpressure: if the DLQ depth alarm fires, the original consumer is configured to stop pulling from orders and pause its consumer group rather than continue piling messages into the DLQ — this shifts the backlog upstream to the broker's orders topic where lag is visible and the pipeline can be paused at the source. For the analytics consumer specifically, its 'DLQ' is a metrics counter, not a topic — it drops and records, accepting data loss as a trade-off for never blocking the pipeline. There is no 'DLQ for the DLQ' — that would be an infinite recursion; instead, depth limits + backpressure + alerting create a bounded failure envelope.
This is a staff-level design exercise testing deep fluency with DLQ patterns: per-consumer topology, transient/permanent error classification, bounded retry with jitter, poison-pill detection via counters + circuit breakers, replay with idempotency and reconciliation, per-consumer SLOs with business-impact-aware alerting, and cascade prevention via depth limits, backpressure, and bounded retention rather than recursive DLQs. The rubric scores each of these facets independently.
In a Kafka-based event processing system, a consumer reads from a main topic with at-least-once delivery semantics. On processing failure, the consumer publishes the failed message to a dead-letter topic and then commits its consumer offset. Assume no transactions or exactly-once semantics are configured, so the DLQ publish and the offset commit are independent operations, and the DLQ topic uses a different key space from the main topic with no cross-topic deduplication.#
Show answer
The message appears twice in the dead-letter topic. The first DLQ publish succeeded before the crash and was not rolled back, because the DLQ write and the offset commit are independent operations with no transaction spanning both. Since the offset was never committed, the consumer re-reads the same message after restart, fails it again, and publishes a second, independent DLQ record. Under at-least-once delivery, the DLQ topic therefore receives a duplicate that consumers of the DLQ must handle, for example by deduplicating on a message identifier.
Under at-least-once delivery, the DLQ publish is a separate producer write to a different topic and is not coupled to the consumer offset commit. The crash after the DLQ write but before the offset commit leaves the DLQ record in place, since no transaction spans the two topics. On restart the uncommitted offset causes the same message to be re-consumed, processing fails again, and a second DLQ record is written. The answer is therefore two independent records for the same message, which is the duplication that at-least-once semantics permit at the DLQ boundary.
Design a dead-letter queue (DLQ) subsystem for a large-scale event-driven microservices platform processing 500K events/second across 200+ microservices. Each microservice consumes from Kafka topics and may fail to process events due to downstream API timeouts, schema incompatibilities, transient infrastructure issues, or business-rule violations. The platform requires: (1) failed events retried with exponential backoff and jitter, (2) a maximum retry limit before events are moved to a terminal dead-letter store, (3) an operational dashboard for on-call engineers to inspect, reprocess, or discard dead-lettered events, (4) a 30-day maximum data retention requirement, and (5) idempotent reprocessing to prevent duplicate side effects. Design the DLQ architecture including topic structure, retry mechanics, metadata tracking, replay/reprocessing strategy, observability, and how you handle the dead-letter-of-the-dead-letter recursion problem.#
Show answer
Topic structure: For each service's main topic events.{service}, use tiered retry topics with increasing delays (for example events.{service}.retry.0 through events.{service}.retry.3, roughly 2s, 10s, 60s, 10min) and a terminal events.{service}.dlq topic. Delays are enforced by a retry-scheduler service that holds messages and republishes them to the source topic when the backoff window elapses, because Kafka has no native per-message delay. The DLQ topic is drained into a durable terminal store (DynamoDB or a database table) for inspection.
Retry mechanics: On failure the consumer computes delay = min(cap, base * 2^attempt) and adds random jitter (for example full jitter in [0, delay]) so that messages failing during a downstream outage do not all retry at the same instant and cause a thundering herd when the dependency recovers. After a maximum retry count (for example 5) the event is published to the DLQ. Errors are classified (timeout, schema_violation, business_rule, infrastructure) so that non-transient failures such as schema or business-rule violations go straight to the DLQ instead of consuming retry budget.
Metadata envelope: Each dead-lettered record wraps the original payload with original_topic, original_partition, original_offset, first_failure_timestamp, last_failure_timestamp, retry_count, error_type, error_message, stack_trace, trace_id/correlation_id, source_service, and consumer_group. This supports filtering by error type, time range, or correlation ID and traceability back to the source record.
Idempotent replay: Every event carries a producer-assigned event_id (UUID) preserved through retries and replay. Consumers record processed event_ids in an idempotency store (a DB table with a unique constraint, or Redis with TTL at least as long as the replay window) and skip side effects for already-processed events. This makes replay safe even if a replay worker crashes mid-batch and re-sends events. Replay is driven from the dashboard by event or offset range: a replay worker reads the DLQ record, unwraps the original payload, and republishes it to the original main topic with its event_id intact, throttled to protect downstream systems.
Dead-letter-of-the-dead-letter recursion: The replay path is itself a consumer and can fail, so it gets a bounded retry budget (for example 3 attempts with backoff). Events that exhaust it move to a terminal store with no consumer and no automatic retry, which is a dead end requiring manual action and structurally prevents infinite loops. Writing to the terminal store raises a high-severity alert. A circuit breaker on the DLQ/replay consumer's failure rate (for example over 10% in 5 minutes) halts replay to avoid cascading failure and pages on-call.
Retention and observability: The DLQ topic and terminal store use time-based retention capped at 30 days (Kafka retention.ms or DynamoDB TTL), with per-topic configuration allowed below that cap. The dashboard offers a filterable paginated list, a detail view with the full envelope and payload, single and batch replay, and discard for unrecoverable events. Metrics include DLQ depth, inflow and outflow rates, age of the oldest message, failure rate by error type, and terminal-store depth, with alerts on depth thresholds, any terminal-store write, and circuit-breaker trips.
This is an open architecture question with no single correct answer, so it stays a SHORT_ANSWER graded by keyword coverage and a reference answer. A strong answer must cover the full DLQ lifecycle: tiered retries with exponential backoff and jitter to avoid thundering herds, a bounded retry limit that routes events to a terminal store, a metadata envelope that preserves traceability to the source record, idempotent replay keyed by a preserved event identifier so repeated replays cause no duplicate side effects, a recursion break for the replay path (bounded retries, a terminal store with no automatic consumer, alerting and a circuit breaker), and retention capped at 30 days with dashboard and metrics support. The reference answer notes that Kafka lacks native per-message delay, so delays need a scheduler service, which avoids an incorrect claim about built-in delayed delivery.
You are building a dead-letter queue (DLQ) subsystem for a high-throughput payment-processing platform that ingests events via Apache Kafka and processes them with a fleet of consumer microservices. Messages that exhaust retries must be routed to a DLQ. Design the complete retry-and-DLQ architecture. Cover: (1) the Kafka topology for primary processing, retry, and dead-letter destinations and the routing logic between them; (2) how you distinguish transient (retryable) failures from poison (non-retryable) messages; (3) how dead-lettered messages are inspected, optionally repaired, and safely replayed back into the main pipeline; (4) the observability metrics and alerting strategy for DLQ health; and (5) how you prevent DLQ processing or replay backpressure from impacting the main pipeline.#
Show answer
Topology: consumers publish failed records to tiered retry topics (for example payments.retry.1m and payments.retry.5m) instead of blocking the main partition. A retry consumer pauses each partition until a record's due time (record timestamp plus exponential backoff, capped at 5 minutes), then processes it and seeks forward only after success; an attempt-count header is incremented on each hop. Kafka retention is not a delay mechanism, so the delay comes from pause/seek logic. Records that exhaust the attempt limit go to payments.dlq. Classification: transient errors (timeouts, 503s, lock contention) are retryable; poison messages (deserialization failure, schema validation error, permanently invalid payload) go straight to the DLQ on first failure, avoiding infinite loops. Each DLQ record carries the original payload, stack trace, failure reason, attempt count, and original topic/partition/offset. Inspection and replay: a DLQ consumer in its own consumer group writes each record to S3 partitioned by date and error class and inserts a Postgres metadata row. Operators inspect via a dashboard, optionally repair, and select batches for a replayer that validates each message against the current schema, checks the idempotency key in a dedup table to prevent double payment, and republishes valid messages to payments.events through a token-bucket rate limiter; schema failures are flagged for manual remediation. Observability: export DLQ produce rate (also as a fraction of main-pipeline throughput), DLQ depth, DLQ consumer lag, p95 message age, and per-error-class counts. Alert when depth exceeds 1,000, p95 age exceeds 1 hour, or produce rate exceeds 10% of main throughput for 5 consecutive minutes, routing to PagerDuty with severity scaled by payment impact. Backpressure isolation: retry and DLQ consumers run in separate consumer groups with independent partition assignment and autoscaling; the replayer is rate-limited; and a circuit breaker past a depth threshold spills new DLQ writes to S3 cold storage with a page, so main-pipeline producers are never blocked.
This is an open architecture question with no single key, so it is graded by a short-answer rubric: a sound answer must cover the retry and DLQ topology with routing rules, a transient-versus-poison classification, schema- and idempotency-gated replay, DLQ health metrics with alert thresholds, and isolation of the DLQ and replay paths from the main pipeline. The reference answer avoids the common misconception that Kafka retention can act as a delay queue; instead it uses pause/seek or tiered retry topics to delay reprocessing without blocking the partition. It names the concrete mechanisms a strong design needs (non-retryable classification, dedup keys before republish, separate consumer groups, token-bucket rate limiting, and a spill-to-cold-storage circuit breaker) so that a candidate can be checked against specific content rather than generic phrases.
Related interview questions
Job market
See system-design salaries and hiring demand from live job postings.
Practise these until they stick
That's every question we hold on this topic, and the page marks what you pick. What it can't do is remember. A free account keeps every answer, and what you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan