← Back to Tutorials
Intermediate⏱️ 30 min

Dead Letter Queues — Handling Failed Messages

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

ReasonDescriptionPrevention
Max Delivery Count exceededMessage delivered N times without completionFix consumer bugs, increase count
TTL expiredMessage lived longer than configured TTLProcess faster, increase TTL
Filter evaluation failureSubscription filter throws errorValidate filter expressions
Queue/topic fullEntity reached max sizeScale up, purge old messages
Explicit dead-letteringCode calls DeadLetterMessageAsync()Intentional — for poison messages

Prerequisites

RequirementDetails
Service Bus NamespaceFrom previous tutorials
Queueorders-queue with max delivery count = 10
.NET SDK8.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.");

Screenshot: Message in DLQ shown in Service Bus Explorer

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}");
}

Screenshot: DLQ message details in console

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

Screenshot: DLQ metric in Azure Monitor

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 ConfigurationValue
MetricDeadletteredMessages
Threshold> 5 messages
Window5 minutes
FrequencyEvery 1 minute
SeveritySev 2 (Warning)

Screenshot: Alert rule configuration in portal

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

Previous: Topics, Subscriptions, and Message Filtering

Next: Sessions and Message Ordering