Skip to content

Dead Letter Exchange — Complete Guide

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Dead Letter Exchange. We cover key concepts, practical examples, and best practices to help you master this topic.

A dead letter exchange routes messages that are rejected, expired, or exceed queue length limits to a dead letter queue for inspection and reprocessing.

Dead Letter Flow

flowchart LR
    P[Producer] --> Q[Main Queue]
    Q -->|"rejected/expired"| DLX[Dead Letter Exchange]
    DLX --> DLQ[Dead Letter Queue]
    DLQ --> O[Operator Review]
    O -->|"replay"| Q
    style DLX fill:#e74c3c,color:#fff
    style DLQ fill:#e74c3c,color:#fff

Configuring Dead Letter Exchange

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='dlx', exchange_type='fanout', durable=True)
channel.queue_declare(queue='dead_letter_queue', durable=True)
channel.queue_bind(exchange='dlx', queue='dead_letter_queue')

args = {
    'x-dead-letter-exchange': 'dlx',
    'x-message-ttl': 60000,
    'x-max-length': 10000,
}
channel.queue_declare(queue='main_queue', durable=True, arguments=args)

print("DLX configured: main_queue -> dlx -> dead_letter_queue")
connection.close()

Expected output:

DLX configured: main_queue -> dlx -> dead_letter_queue

When Messages Go to DLX

Messages are dead-lettered when:

  1. Consumer rejects with basic.reject or basic.nack and requeue=false
  2. Message TTL expires (x-message-ttl)
  3. Queue length limit exceeded (x-max-length or x-max-length-bytes)
  4. Message is returned to queue from a consumer that has been cancelled

Consumer with DLQ Routing

import pika, json

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='main_queue', durable=True)
channel.basic_qos(prefetch_count=1)

def callback(ch, method, properties, body):
    data = json.loads(body)
    print(f"Processing: {data['id']}")

    if not data.get('valid', True):
        print(f"  Invalid, sending to DLQ")
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
        return

    ch.basic_ack(delivery_tag=method.delivery_tag)
    print(f"  Done")

channel.basic_consume(queue='main_queue', on_message_callback=callback)
channel.start_consuming()

Expected output:

Processing: 1
  Done
Processing: 2
  Invalid, sending to DLQ

Replaying from DLQ

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.queue_declare(queue='dead_letter_queue', durable=True)

def replay(ch, method, properties, body):
    print(f"Replaying: {body.decode()}")
    ch.basic_publish(exchange='', routing_key='main_queue', body=body,
                     properties=pika.BasicProperties(delivery_mode=2))
    ch.basic_ack(delivery_tag=method.delivery_tag)
    print("Replayed")

ch.basic_consume(queue='dead_letter_queue', on_message_callback=replay)
print("Waiting for DLQ messages...")
ch.start_consuming()

Common Mistakes

1. Not Setting Requeue=False

reject with requeue=true returns the message to the original queue, not to the DLX. Use requeue=false for DLX routing.

2. No DLX Configured

Without x-dead-letter-exchange, rejected messages are discarded. Always configure DLX for production queues.

3. Circular Dead Lettering

If the DLQ routes back to the main queue via its own DLX, messages loop forever. Ensure DLQs have no DLX configured.

4. Not Monitoring the DLQ

Messages accumulate in the DLQ silently. Set up alerts when DLQ depth exceeds a threshold.

5. Replaying Without Fixing

Replaying a message with the same invalid data fails again. Fix the data or the consumer before replaying.

Practice Questions

1. When does RabbitMQ send a message to the DLX?

When a consumer rejects with requeue=false, message TTL expires, queue length limit is exceeded, or the consumer is cancelled.

2. What is the difference between DLX and DLQ?

DLX is the exchange that receives dead messages. DLQ is the queue bound to the DLX that stores dead messages. The DLX routes to the DLQ.

3. How do you prevent circular dead lettering?

Do not configure x-dead-letter-exchange on the DLQ. If the DLQ has a DLX, dead messages from the DLQ go back to the main queue.

4. Can I set a max retry count before dead lettering?

RabbitMQ does not natively track retry counts. Use custom message headers (x-retry-count) in the consumer to track and reject after N attempts.

Challenge

Design a dead letter Strategy for an order processing system: orders that fail validation go to DLQ immediately. Orders that timeout during payment processing are retried 3 times before going to DLQ. DLQ messages are reviewed and replayed via a management tool.

FAQ

Can I set different DLX per queue?

Yes. Each queue can have its own x-dead-letter-exchange. Different queues can have different DLX configurations.

Does DLX preserve message headers?

Yes. When a message is dead-lettered, its original headers and properties are preserved. RabbitMQ adds x-death header with the reason.

How do I inspect the reason a message was dead-lettered?

Check the x-death header array on DLQ messages. It contains the reason (rejected, expired, maxlen), queue, and timestamp.

Can I dead-letter to a different virtual host?

No. DLX must be in the same virtual host as the source queue.

Does DLX work with quorum queues?

Yes. Quorum queues support dead letter exchanges. The configuration is the same as classic queues.

Mini Project: Dead Letter Inspector

import pika, json

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.queue_declare(queue='dead_letter_queue', durable=True)

def inspect_dlq():
    methods = []
    while True:
        method, properties, body = ch.basic_get(queue='dead_letter_queue', auto_ack=False)
        if not method:
            break

        x_death = properties.headers.get('x-death', [{}])[0] if properties.headers else {}
        entry = {
            'body': json.loads(body) if body else {},
            'reason': x_death.get('reason', 'unknown'),
            'routing_key': method.routing_key,
            'delivery_tag': method.delivery_tag,
        }
        methods.append(entry)

    print(f"DLQ contains {len(methods)} messages:")
    for m in methods:
        print(f"  Reason: {m['reason']} | Body: {m['body']}")

inspect_dlq()

Expected output:

DLQ contains 2 messages:
  Reason: rejected | Body: {'id': 'ORD-456', 'valid': False}
  Reason: expired | Body: {'id': 'ORD-789'}

What's Next

Now that you understand dead letter exchange, explore message TTL for time-based expiry, then learn about queue TTL for auto-deleting idle queues.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro