Skip to content
API Design Learn/Event-Driven APIs
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

Dead Letter Queues & Retry Policies

7 min read

You'll learn to

  • -Design a bounded retry policy with exponential backoff instead of retrying forever or not at all
  • -Route permanently-failing messages to a dead letter queue so one poison message never blocks an entire stream

A message consumer will eventually encounter a message it genuinely cannot process - malformed data, a downstream dependency that's been unavailable for hours, a bug that makes one specific message always crash the handler. Without a deliberate policy for this, that one message can block every message behind it in the same stream indefinitely.

Retry With Exponential Backoff

Retrying with increasing delays instead of immediately and repeatedly
import time

def process_with_retry(message, max_attempts=5):
    for attempt in range(1, max_attempts + 1):
        try:
            handle_message(message)
            return  # success
        except TransientError:
            if attempt == max_attempts:
                send_to_dead_letter_queue(message)
                return
            delay = min(2 ** attempt, 60)  # 2s, 4s, 8s, 16s, capped at 60s
            time.sleep(delay)

Retrying immediately and repeatedly on failure can make a transient problem worse - hammering an already-struggling downstream dependency with immediate retries adds exactly the load it can least handle right now. Exponential backoff (each retry waiting longer than the last, typically capped at some maximum) gives a transient failure genuine time to resolve, rather than retrying at a fixed, aggressive interval regardless of whether the underlying problem has had any chance to recover.

Dead Letter Queues: Isolating What Can't Be Fixed by Retrying

After exhausting the retry budget, a message that still fails gets moved to a dead letter queue (DLQ) - a separate holding area for messages that need human investigation, rather than being retried forever or silently dropped. Critically, moving a message to the DLQ removes it from the main stream, so one permanently-broken message doesn't block every legitimate message queued behind it from being processed.

Not Every Failure Deserves the Same Retry Treatment

A transient failure (a downstream timeout, a temporary network blip) genuinely benefits from retrying - the same operation might well succeed on the next attempt. A message that fails validation, or triggers a deterministic bug in the handler, will fail identically on every retry - retrying it five times with backoff just delays the inevitable move to the DLQ by however long the backoff schedule takes. Distinguishing these two failure categories (the same retriable-vs-non-retriable distinction from the gRPC error-handling chapter) and routing permanent failures to the DLQ immediately, without wasting the full retry budget, is a meaningful optimization worth making explicit.

A DLQ that nobody actually monitors is just a quieter way to silently lose messages - the pattern only delivers its real value paired with alerting and a genuine operational process for investigating and, where appropriate, replaying DLQ messages once the underlying issue is fixed.

Interview Signal is part of Pro

See a real weak answer next to a real strong one for this exact topic.

Quiz is part of Pro

Test what you just read with a short quiz, and bank the XP.

Ready to Build This?

Design Dead Letter Detective in the API Design Lab's gRPC & Event-Driven act.

ScaleDojo Logo
Initializing ScaleDojo