Dead Letter Queues — Handling Failed Messages
What is a Dead Letter Queue (DLQ)?
A Dead Letter Queue is a secondary sub-queue attached to every Service Bus queue and subscription. Messages that cannot be delivered or processed are automatically moved here for inspection and remediation.
┌──────────────────────────────────────────────────────────────┐
│ orders-queue │
│ │
│ [Msg1] [Msg2] [Msg3] ───────────────────▶ Consumer │
│ │ │
│ Processing fails │
│ (max delivery count) │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ orders-queue/$DeadLetterQueue │ │
│ │ [FailedMsg1] [FailedMsg2] │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Why Messages Go to the DLQ
| Reason | Description | Prevention |
|---|---|---|
| Max Delivery Count exceeded | Message delivered N times without completion | Fix consumer bugs, increase count |
| TTL expired | Message lived longer than configured TTL | Process faster, increase TTL |
| Filter evaluation failure | Subscription filter throws error | Validate filter expressions |
| Queue/topic full | Entity reached max size | Scale up, purge old messages |
| Explicit dead-lettering | Code calls DeadLetterMessageAsync() | Intentional — for poison messages |
Prerequisites
| Requirement | Details |
|---|---|
| Service Bus Namespace | From previous tutorials |
| Queue | orders-queue with max delivery count = 10 |
| .NET SDK | 8.0+ with Azure.Messaging.ServiceBus |
Step 1 — Trigger Dead-Lettering (Max Delivery Count)
using Azure.Messaging.ServiceBus;
const string connectionString = "<YOUR_CONNECTION_STRING>";
const string queueName = "orders-queue";
await using var client = new ServiceBusClient(connectionString);
// Send a "poison" message
ServiceBusSender sender = client.CreateSender(queueName);
await sender.SendMessageAsync(new ServiceBusMessage("Poison message"));
// Simulate repeated processing failures
ServiceBusReceiver receiver = client.CreateReceiver(queueName);
for (int attempt = 1; attempt <= 10; attempt++)
{
var msg = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
if (msg != null)
{
Console.WriteLine($"Attempt {attempt}: Abandoning message (delivery count: {msg.DeliveryCount})");
await receiver.AbandonMessageAsync(msg);
}
}
// After max delivery count, message is in DLQ
Console.WriteLine("Message moved to Dead Letter Queue.");

Step 2 — Read from the Dead Letter Queue
The DLQ path is: <queue-name>/$DeadLetterQueue
using Azure.Messaging.ServiceBus;
const string connectionString = "<YOUR_CONNECTION_STRING>";
const string queueName = "orders-queue";
await using var client = new ServiceBusClient(connectionString);
ServiceBusReceiver dlqReceiver = client.CreateReceiver(
queueName,
new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter });
IReadOnlyList<ServiceBusReceivedMessage> deadLetters =
await dlqReceiver.ReceiveMessagesAsync(maxMessages: 10, maxWaitTime: TimeSpan.FromSeconds(5));
foreach (var msg in deadLetters)
{
Console.WriteLine($"DLQ Message: {msg.Body}");
Console.WriteLine($" Reason: {msg.DeadLetterReason}");
Console.WriteLine($" Description: {msg.DeadLetterErrorDescription}");
Console.WriteLine($" Enqueued: {msg.EnqueuedTime}");
Console.WriteLine($" Delivery Count: {msg.DeliveryCount}");
}

Step 3 — Resubmit DLQ Messages
using Azure.Messaging.ServiceBus;
const string connectionString = "<YOUR_CONNECTION_STRING>";
const string queueName = "orders-queue";
await using var client = new ServiceBusClient(connectionString);
ServiceBusReceiver dlqReceiver = client.CreateReceiver(
queueName,
new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter });
ServiceBusSender sender = client.CreateSender(queueName);
var deadLetters = await dlqReceiver.ReceiveMessagesAsync(maxMessages: 10, maxWaitTime: TimeSpan.FromSeconds(5));
foreach (var msg in deadLetters)
{
// Create a new message from the dead-lettered one
var resubmit = new ServiceBusMessage(msg.Body)
{
ContentType = msg.ContentType,
Subject = msg.Subject,
MessageId = msg.MessageId,
CorrelationId = msg.CorrelationId
};
// Copy application properties
foreach (var prop in msg.ApplicationProperties)
{
resubmit.ApplicationProperties.Add(prop.Key, prop.Value);
}
await sender.SendMessageAsync(resubmit);
await dlqReceiver.CompleteMessageAsync(msg);
Console.WriteLine($"Resubmitted: {msg.MessageId}");
}
Step 4 — Monitor DLQ Depth
Via Azure CLI
az servicebus queue show \
--name orders-queue \
--namespace-name sb-demo-namespace \
--resource-group rg-servicebus-demo \
--query "countDetails.deadLetterMessageCount"
Via Azure Monitor Metrics
az monitor metrics list \
--resource /subscriptions/<SUB_ID>/resourceGroups/rg-servicebus-demo/providers/Microsoft.ServiceBus/namespaces/sb-demo-namespace \
--metric "DeadletteredMessages" \
--interval PT5M

Step 5 — Alert on DLQ Growth
az monitor metrics alert create \
--name "DLQ-Alert-OrdersQueue" \
--resource-group rg-servicebus-demo \
--scopes /subscriptions/<SUB_ID>/resourceGroups/rg-servicebus-demo/providers/Microsoft.ServiceBus/namespaces/sb-demo-namespace \
--condition "total DeadletteredMessages > 5" \
--window-size PT5M \
--evaluation-frequency PT1M \
--action-group <ACTION_GROUP_ID> \
--description "Alert when DLQ messages exceed 5 in orders-queue"
| Alert Configuration | Value |
|---|---|
| Metric | DeadletteredMessages |
| Threshold | > 5 messages |
| Window | 5 minutes |
| Frequency | Every 1 minute |
| Severity | Sev 2 (Warning) |

DLQ Handling Strategy
┌─────────────────────────────────────────────────────────┐
│ DLQ Processing Pipeline │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. Alert fires (DLQ depth > threshold) │
│ │ │
│ ▼ │
│ 2. Inspect DLQ messages (reason, description) │
│ │ │
│ ├──▶ Transient error? → Resubmit to queue │
│ │ │
│ ├──▶ Bug in consumer? → Fix code, resubmit │
│ │ │
│ └──▶ Poison message? → Log, archive, discard │
│ │
└─────────────────────────────────────────────────────────┘
Summary
- ✅ Understood what DLQ is and why messages end up there
- ✅ Triggered dead-lettering via max delivery count
- ✅ Read and inspected DLQ messages programmatically
- ✅ Resubmitted DLQ messages back to the main queue
- ✅ Set up monitoring and alerting on DLQ depth