RabbitMQ
Dead Letter Queue
Created: 05 Jan 2026
Updated: 05 Jan 2026
Dead Letter Queue
What is a Dead Letter Queue?
A Dead Letter Queue is a standard queue used to store messages that have failed to be processed successfully for specific reasons. Instead of simply dropping the message or letting it block the main queue indefinitely, RabbitMQ "dead-letters" it—routing it to a specialized exchange and then to a dedicated queue for later inspection or retry.
Why do Messages become "Dead Letters"?
A message is moved to a DLQ when one of the following events occurs:
- Negative Acknowledgement: The consumer rejects the message using
basic.rejectorbasic.nackwith therequeueparameter set tofalse. - TTL Expiration: The message expires because its Time-To-Live (TTL) has passed.
- Queue Length Limit: The message is dropped because the queue has reached its maximum length limit.
Configuration Steps
To set up a DLQ, you typically follow these steps:
- Create the Dead Letter Exchange: A standard exchange (usually of type
directorfanout). - Create the Dead Letter Queue: A standard queue to hold the failed messages.
- Bind them: Connect the DLQ to the DLX.
- Configure the Main Queue: When declaring your primary queue, you must provide the
x-dead-letter-exchangeargument pointing to your DLX.
Note: You can also optionally provide x-dead-letter-routing-key if you want the dead-lettered message to use a different routing key than the original one.Code Example
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
// 1. Connection Setup
var factory = new ConnectionFactory() { HostName = "localhost" };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();
// Constant Definitions
const string MainExchange = "main-exchange";
const string MainQueue = "main-queue";
const string MainRoutingKey = "order.created";
const string DeadLetterExchange = "dead-letter-exchange";
const string DeadLetterQueue = "dead-letter-queue";
const string DeadLetterRoutingKey = "failed";
// --- INFRASTRUCTURE SETUP ---
// 2. Declare Dead Letter Exchange and Queue
// It is best practice to declare the DLX components first.
await channel.ExchangeDeclareAsync(
exchange: DeadLetterExchange,
type: ExchangeType.Direct,
durable: true,
autoDelete: false
);
await channel.QueueDeclareAsync(
queue: DeadLetterQueue,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null
);
await channel.QueueBindAsync(
queue: DeadLetterQueue,
exchange: DeadLetterExchange,
routingKey: DeadLetterRoutingKey
);
// 3. Declare Main Exchange and Main Queue with DLX arguments
await channel.ExchangeDeclareAsync(
exchange: MainExchange,
type: ExchangeType.Direct,
durable: true,
autoDelete: false
);
// Linking the Main Queue to the Dead Letter Exchange
var mainQueueArguments = new Dictionary<string, object?>
{
{ "x-dead-letter-exchange", DeadLetterExchange },
{ "x-dead-letter-routing-key", DeadLetterRoutingKey },
{ "x-message-ttl", 10000 } // 10 seconds TTL for testing purposes
};
await channel.QueueDeclareAsync(
queue: MainQueue,
durable: true,
exclusive: false,
autoDelete: false,
arguments: mainQueueArguments
);
await channel.QueueBindAsync(
queue: MainQueue,
exchange: MainExchange,
routingKey: MainRoutingKey
);
// --- PRODUCER: Publishing a Message ---
string message = $"New Order Created - Timestamp: {DateTime.Now.ToLongTimeString()}";
var body = Encoding.UTF8.GetBytes(message);
await channel.BasicPublishAsync(
exchange: MainExchange,
routingKey: MainRoutingKey,
body: body
);
Console.WriteLine($" [x] Sent: '{message}'");
Console.WriteLine(" [!] The message will move to the DLQ in 10 seconds due to TTL (No consumer on Main Queue).");
// --- CONSUMER: Monitoring the Dead Letter Queue ---
var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (model, ea) =>
{
var receivedBody = ea.Body.ToArray();
var receivedMessage = Encoding.UTF8.GetString(receivedBody);
Console.WriteLine("\n--------------------------------------------------");
Console.WriteLine($" [DLQ RECEIVE] Message moved to DLQ: {receivedMessage}");
Console.WriteLine(" Reason: Likely TTL expiration or rejection.");
Console.WriteLine("--------------------------------------------------");
await Task.CompletedTask;
};
await channel.BasicConsumeAsync(queue: DeadLetterQueue, autoAck: true, consumer: consumer);
Console.WriteLine("\n Press [enter] to exit.");
Console.ReadLine();