Post

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.