Open App

Azure Service Bus Dead-Letter Queues (DLQ)

Every queue and subscription in Azure Service Bus has a secondary, system-managed sub-queue called a dead-letter queue (DLQ). The DLQ stores messages that cannot be successfully delivered or processed by consumers. Understanding the root causes of dead-lettering helps you resolve pipeline failures.

Common Causes of Dead-Lettering

Azure Service Bus automatically routes messages to the DLQ when certain conditions are met:

1. Max Delivery Count Exceeded

When a consumer locks a message but fails to complete or abandon it successfully, the broker releases the lock and increments the message's delivery count. If this count exceeds the queue's configured MaxDeliveryCount (default is 10), the message is dead-lettered with the reason: MaxDeliveryCountExceeded.

Common cause: Consumer application crashes, runs into unhandled exceptions, or times out during processing.

2. Time-To-Live (TTL) Expiration

If a message remains in the active queue longer than its configured Time-To-Live (TTL) duration, and dead-lettering on expiration is enabled (EnableDeadLetteringOnMessageExpiration), the message is moved to the DLQ with the reason: TTLExpiredException.

Common cause: Consumer service downtime or message backlogs leading to slow processing rates.

3. Explicit Application Dead-Lettering

Consumer applications can explicitly move a message to the DLQ by calling the settlement action. This is useful when the message body contains malformed JSON/XML, fails business validation, or is otherwise unprocessable.

Common cause: Data schema mismatches or invalid input data.

4. Header and Metadata Size Exceeded

If the cumulative size of a message's custom properties and headers exceeds the protocol limits, Azure Service Bus moves the message to the DLQ with the reason HeaderSizeExceeded.

Common cause: Diagnostic context, correlation identifiers, or retry metadata appended to a message on every hop through a pipeline, growing the header until it breaches the limit.

5. Subscription Rule Errors

On a topic subscription, a SQL filter or rule action is evaluated against every incoming message. If the expression throws, usually because it references a property that is missing, or compares incompatible types. In that case the broker dead-letters the message rather than dropping it.

Common cause: A rule such as priority > 5 where some publishers omit priority entirely, or send it as a string.

6. Auto-Forwarding Failures

When a queue or subscription auto-forwards to a destination that no longer exists, has been disabled, or is full, the message is not dead-lettered to the normal DLQ. It goes to the transfer dead-letter queue of the source entity, at the path $Transfer/$DeadLetterQueue. This one catches people out: the messages are not where you would first look. See auto-forwarding for the full chain semantics.

Reading the Dead-Letter Reason

When Azure dead-letters a message it annotates it with system properties describing what happened. These are the first thing to check, and they are ordinary message properties you can read from any client:

PropertyWhat it tells you
DeadLetterReason The category: MaxDeliveryCountExceeded, TTLExpiredException, HeaderSizeExceeded, or a custom string your own consumer supplied.
DeadLetterErrorDescription The detail. When a consumer dead-letters explicitly this often carries the exception message, which is usually the single most useful field.
DeadLetterSource Present when the message was dead-lettered from a different entity and then forwarded. Without it, a mixed DLQ gives no hint where a message originated.
DeliveryCount How many delivery attempts were made. A count equal to MaxDeliveryCount confirms retry exhaustion rather than an explicit rejection.
EnqueuedTimeUtc When the message first arrived. Compared against the dead-letter time, this separates "failed immediately" from "sat around and expired".

A useful first pass is to group a DLQ by DeadLetterReason. A backlog that is entirely MaxDeliveryCountExceeded points at the consumer; a mixture of custom reasons points at the data.

Diagnosing by Symptom

Everything dead-lettered at once

A sharp spike with a single reason almost always means a deployment or a downstream dependency, not the messages. Check EnqueuedTimeUtc against your release history. These are usually safe to replay unchanged once the dependency recovers.

A slow trickle over days

A steady drip of dead-letters with varied payloads suggests a data problem: a subset of messages hits an edge case the consumer mishandles. Inspect several bodies and look for what they share rather than what makes each unique.

Delivery count is 1

The message was rejected outright, not retried. Either a consumer dead-lettered it deliberately, or it never reached one, because of a rule error or a header limit. DeadLetterReason distinguishes them.

The DLQ is empty but messages vanished

Check the transfer dead-letter queue on the source entity, and confirm TTL expiry is set to dead-letter rather than to drop silently. Without EnableDeadLetteringOnMessageExpiration, expired messages are simply gone.

Replaying Safely

Resubmitting a dead-lettered message means sending a copy back to the source queue. A few things are worth knowing before you replay a large batch:

Reducing Dead-Letters in the First Place

One operational detail worth remembering: dead-lettered messages count toward the entity's size quota, and they do not expire on their own. TTL does not apply once a message is in the DLQ. An ignored dead-letter queue will eventually fill the entity and start rejecting new sends. It needs draining, not just watching.

Frequently Asked Questions

Where is the dead-letter queue located?

It is a system sub-queue of the entity it belongs to. For a queue the path is <queue-name>/$deadletterqueue; for a subscription it is <topic>/Subscriptions/<subscription>/$deadletterqueue. You do not create it, and it always exists, and it cannot be deleted independently of its parent.

Do dead-lettered messages expire?

No. Time-to-live is not applied to messages once they are in the dead-letter queue, so they remain until something removes them. They also count against the entity's storage quota, which means an unattended DLQ can eventually cause the parent queue to reject new messages.

Can I move a message back to the original queue directly?

Not as a broker-side move. Azure Service Bus has no "return to source" operation. Resubmission means reading the message, sending a copy to the source queue, and then removing the original. Tools that offer a one-click replay are performing exactly those steps for you.

Why is my dead-letter queue empty when messages are clearly missing?

Three common reasons: the message expired while EnableDeadLetteringOnMessageExpiration was off, so it was dropped silently; it failed auto-forwarding and is in the transfer dead-letter queue of the source entity; or on a topic, no subscription rule matched, so it was never delivered anywhere.

Does peeking a dead-lettered message change it?

No. Peek is a read-only operation. It does not lock the message, consume it, or increment its delivery count. You can inspect an entire dead-letter queue without altering any of it.

Inspecting and Replaying DLQ Messages

Unlike active queues, where consumers actively pull messages, the dead-letter queue is a storage area. Inspecting it requires peeking or locking messages to determine why they failed, and then resubmitting them once the consumer or payload issue is fixed.

Screenshot showing dead-letter queue message inspection details and resubmit operations
Inspect dead-letter reasons, error headers, and message payloads directly in Bussin. (Click to zoom)

Bussin makes managing dead-letter queues straightforward: