Dead Letter Queues — Real Usage Patterns
Dead letter queues with real-world usage patterns
Dead letter queues (DLQs) are not just a place to dump poison messages. They are an operational safety net that should encode why a message failed and what to do next. This guide covers concrete patterns for RabbitMQ and .NET consumers.
Prerequisites
- RabbitMQ 3.12+
- .NET 8 SDK
- Basic understanding of exchanges, queues, and routing keys
Core DLQ patterns
- Poison message isolation: move messages that fail after N retries.
- Delayed retry: dead-letter to a retry queue with TTL, then re-route.
- Inspection workflow: enrich the message with failure metadata for operators.
Declare DLQ and retry queues
1
2
3
4
5
6
7
8
9
10
var args = new Dictionary<string, object>
{
{ "x-dead-letter-exchange", "orders.dlx" },
{ "x-dead-letter-routing-key", "orders.failed" }
};
channel.ExchangeDeclare("orders.dlx", ExchangeType.Direct, durable: true);
channel.QueueDeclare("orders", durable: true, exclusive: false, autoDelete: false, arguments: args);
channel.QueueDeclare("orders.dlq", durable: true, exclusive: false, autoDelete: false);
channel.QueueBind("orders.dlq", "orders.dlx", "orders.failed");
Publish failure metadata
When you reject a message, include headers describing the failure so that the DLQ is actionable.
1
2
3
4
5
6
7
8
void RejectWithMetadata(BasicDeliverEventArgs ea, string errorCode)
{
ea.BasicProperties.Headers ??= new Dictionary<string, object>();
ea.BasicProperties.Headers["error_code"] = errorCode;
ea.BasicProperties.Headers["failed_at"] = DateTimeOffset.UtcNow.ToString("O");
channel.BasicReject(ea.DeliveryTag, requeue: false);
}
Implement a delayed retry cycle
Create a retry queue with TTL and dead-letter it back to the main queue.
1
2
3
4
5
6
7
8
var retryArgs = new Dictionary<string, object>
{
{ "x-message-ttl", 30000 },
{ "x-dead-letter-exchange", "" },
{ "x-dead-letter-routing-key", "orders" }
};
channel.QueueDeclare("orders.retry", durable: true, exclusive: false, autoDelete: false, arguments: retryArgs);
Operational best practices
- Track retry counts in headers to prevent infinite loops.
- Provide a replay tool that allows selective reprocessing.
- Alert on DLQ growth and processing latency.
Things to remember
- DLQs are part of the normal failure workflow, not a last resort.
- Enrich dead-lettered messages to make them actionable.
- Separate retry queues from the DLQ so operators can differentiate transient and permanent failures.
This post is licensed under CC BY 4.0 by the author.