Service Bus Dead Letter Queue

The Dead Letter Queue (DLQ) is a special sub-queue that automatically captures messages that could not be delivered or processed successfully. Instead of losing these problematic messages, Service Bus moves them to the DLQ so developers can investigate, fix the root cause, and replay them if needed. Every queue and every topic subscription has its own built-in Dead Letter Queue.

Why the Dead Letter Queue Exists

In any messaging system, some messages will fail — corrupted data, wrong format, expired messages, or failed consumer logic. Without a DLQ, these messages either get lost or loop forever. The DLQ acts as a safety net that catches all problem messages in one place.

Normal flow:
[Queue] --> [Consumer] --> message processed --> DELETED

Problem flow WITHOUT DLQ:
[Queue] --> [Consumer] --> fails 10 times --> message LOST forever

Problem flow WITH DLQ:
[Queue] --> [Consumer] --> fails 10 times --> message moves to [DLQ]
[DLQ]   --> [Developer] investigates --> fixes issue --> replays message

Dead Letter Queue Path

The DLQ is a sub-queue attached to every queue and subscription. Access it using a special path format.

Entity TypeDLQ Path FormatExample
Queue DLQqueue-name/$DeadLetterQueueorders/$DeadLetterQueue
Subscription DLQtopic-name/subscriptions/sub-name/$DeadLetterQueueorder-events/subscriptions/inventory-sub/$DeadLetterQueue

Reasons Messages Go to the Dead Letter Queue

ReasonTriggerDeadLetterReason Value
Max Delivery Count ExceededMessage failed more than MaxDeliveryCount timesMaxDeliveryCountExceeded
Message TTL ExpiredMessage expired before a consumer read it (if DL on expiry is enabled)TTLExpiredException
Header Size ExceededMessage properties exceed the allowed header sizeHeaderSizeExceeded
Manual Dead-LetterConsumer explicitly called DeadLetterMessageAsync()Custom reason set by developer
Session Lock LostSession lock expired during processingSessionLockLost
Filter Evaluation ErrorSubscription filter threw an error evaluating the messageFilterEvalError

Dead Letter on Message Expiry

By default, expired messages are silently deleted. Enabling Dead-Lettering on Message Expiration sends expired messages to the DLQ instead, preserving them for investigation.

az servicebus queue create \
  --resource-group rg-messaging-prod \
  --namespace-name myshopns \
  --name orders \
  --enable-dead-lettering-on-message-expiration true

Enable DLQ on a Topic Subscription

az servicebus topic subscription create \
  --resource-group rg-messaging-prod \
  --namespace-name myshopns \
  --topic-name order-events \
  --name inventory-sub \
  --enable-dead-lettering-on-message-expiration true \
  --dead-letter-on-filter-evaluation-exceptions true

Reading from the Dead Letter Queue — .NET

Read from Queue DLQ

using Azure.Messaging.ServiceBus;

await using var client = new ServiceBusClient(connectionString);

// Access the DLQ using the SubQueue.DeadLetter option
await using var dlqReceiver = client.CreateReceiver(
    "orders",
    new ServiceBusReceiverOptions
    {
        SubQueue = SubQueue.DeadLetter
    }
);

ServiceBusReceivedMessage dlqMsg = await dlqReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));

if (dlqMsg != null)
{
    Console.WriteLine($"DLQ Message Body : {dlqMsg.Body}");
    Console.WriteLine($"Dead Letter Reason: {dlqMsg.DeadLetterReason}");
    Console.WriteLine($"Dead Letter Description: {dlqMsg.DeadLetterErrorDescription}");
    Console.WriteLine($"Delivery Count: {dlqMsg.DeliveryCount}");

    // Complete the DLQ message (remove it after investigation)
    await dlqReceiver.CompleteMessageAsync(dlqMsg);
}

Read from Topic Subscription DLQ

await using var dlqReceiver = client.CreateReceiver(
    "order-events",
    "inventory-sub",
    new ServiceBusReceiverOptions
    {
        SubQueue = SubQueue.DeadLetter
    }
);

var dlqMsg = await dlqReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));

if (dlqMsg != null)
{
    Console.WriteLine($"DLQ body: {dlqMsg.Body}");
    Console.WriteLine($"Reason: {dlqMsg.DeadLetterReason}");
    await dlqReceiver.CompleteMessageAsync(dlqMsg);
}

Manually Dead-Lettering a Message

await using var receiver = client.CreateReceiver("orders");

var msg = await receiver.ReceiveMessageAsync();

if (msg != null)
{
    string body = msg.Body.ToString();

    if (!body.Contains("orderId"))
    {
        // Message is malformed — send it to DLQ immediately
        await receiver.DeadLetterMessageAsync(
            msg,
            deadLetterReason: "SchemaValidationFailed",
            deadLetterErrorDescription: "Message body is missing required field: orderId"
        );
        Console.WriteLine("Invalid message dead-lettered.");
    }
    else
    {
        // Process and complete valid message
        Console.WriteLine($"Processing valid order: {body}");
        await receiver.CompleteMessageAsync(msg);
    }
}

Replaying Dead-Lettered Messages

After fixing the root cause — a bug in the consumer or a data quality issue — messages in the DLQ can be replayed by reading them from the DLQ and re-sending them to the original queue or topic.

await using var dlqReceiver = client.CreateReceiver(
    "orders",
    new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter }
);

await using var sender = client.CreateSender("orders");

while (true)
{
    var dlqMsg = await dlqReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(3));
    if (dlqMsg == null) break;

    // Create a fresh message from the DLQ message body
    var replayMsg = new ServiceBusMessage(dlqMsg.Body)
    {
        MessageId   = dlqMsg.MessageId,
        ContentType = dlqMsg.ContentType,
        Subject     = dlqMsg.Subject
    };

    // Copy custom properties
    foreach (var prop in dlqMsg.ApplicationProperties)
        replayMsg.ApplicationProperties[prop.Key] = prop.Value;

    // Send back to the original queue
    await sender.SendMessageAsync(replayMsg);
    Console.WriteLine($"Replayed message: {dlqMsg.MessageId}");

    // Remove from DLQ
    await dlqReceiver.CompleteMessageAsync(dlqMsg);
}

Console.WriteLine("All DLQ messages replayed.");

Monitoring the Dead Letter Queue

A growing DLQ message count indicates a systemic problem in the consumer or the data being sent. Monitor the DLQ count using Azure Monitor or CLI.

Check DLQ Message Count via CLI

az servicebus queue show \
  --resource-group rg-messaging-prod \
  --namespace-name myshopns \
  --name orders \
  --query "countDetails.deadLetterMessageCount"

Set an Alert When DLQ Count Exceeds Threshold

DLQ Count Alert:
  Metric  : Dead-lettered message count
  Condition: Greater than 100
  Action  : Send email to ops-team@company.com
  Severity: 2 (Warning)

DLQ Best Practices

  • Always enable Dead-Lettering on Message Expiration — do not silently lose expired messages
  • Set a meaningful deadLetterReason when manually dead-lettering — this saves investigation time
  • Build a DLQ monitor dashboard in Azure Monitor and alert when message count rises
  • Create a replay tool that can move messages from DLQ back to the main queue after a fix is deployed
  • Set the MaxDeliveryCount based on processing complexity — low for fast processing, higher for complex retry logic
  • Never ignore the DLQ — every message in the DLQ represents a missed business transaction

Summary

The Dead Letter Queue is a built-in safety net in Azure Service Bus. It captures messages that exceed retry limits, expire, fail filter evaluation, or are manually rejected by consumers. The DLQ path uses the $DeadLetterQueue suffix. Messages include DeadLetterReason and DeadLetterErrorDescription properties that explain why they failed. After fixing the root cause, messages from the DLQ can be replayed to the original queue. Monitoring and alerting on DLQ growth prevents silent message loss in production systems.

Leave a Comment