Products · DLQ Manager

Best practices for clean dead-letters

Most dead-letter pain is self-inflicted at the consumer. A few small habits make every dead-letter carry its real cause - which is what turns a 2 a.m. queue of "MaxDeliveryCountExceeded" into a five-minute triage. Here's what we tell our own users, and what the product nudges you toward.

The one trap that hides every real error

The most common mistake is letting an exception bubble out of the handler. Service Bus retries the message until it exhausts its delivery count, then dead-letters it with the reason MaxDeliveryCountExceeded - and the actual exception, the one that would tell you what to fix, is gone. Every message in the queue looks identical.

Instead, catch the failure and dead-letter it explicitly, with the exception as the reason. Now the real cause rides along on the dead-letter copy, where DLQ Manager (and you) can read it:

// Azure Functions trigger, or your own SDK processor
try
{
    await Process(msg.Body);
    await actions.CompleteMessageAsync(msg);
}
catch (Exception ex)
{
    // Dead-letter EXPLICITLY with the real reason - do not just
    // rethrow and let redelivery bury it as MaxDeliveryCountExceeded.
    await actions.DeadLetterMessageAsync(msg,
        deadLetterReason: ex.GetType().Name,
        deadLetterErrorDescription: ex.Message);
}

Or let one line do it for you

We ship a tiny client library so you don't have to hand-roll the catch block. It dead-letters a crash with the exception type as the reason, the message as the description and the stack as properties - while transient faults still retry - and it preserves the message's trace id, so DLQ Manager can link the dead-letter straight to the consumer's own exception in your Application Insights.

  • Wraps a ServiceBusProcessor handler or an Azure Functions catch block.
  • Depends only on Azure.Messaging.ServiceBus; targets netstandard2.0.
  • Transient faults keep retrying - only real failures get dead-lettered.
dotnet add package OpsChisel.DlqManager.Client

// ServiceBusProcessor: one line
processor.ProcessMessageAsync += DeadLetterOnFailure(HandleAsync);

// Azure Functions: in the catch block
var d = DeadLetterDetails.From(ex);
await actions.DeadLetterMessageAsync(
    msg, d.Properties, d.Reason, d.Description);

Four more habits that pay off

Tune delivery attempts and lock duration

Set the max delivery count to what actually helps - enough for a transient blip, not so many that a poison message churns for minutes. Match the lock duration to your real processing time so a slow handler isn't mistaken for a crash. DLQ Manager's delivery-pattern diagnosis reads these to tell crash from lock-expiry from slow retry.

Keep the trace id, label the message

Carry a Diagnostic-Id (W3C traceparent) end to end so a dead-letter can be matched to the exact request that failed. A meaningful subject/label and a stable business key make failure clustering and the sibling-comparison check far sharper.

Guard the retry loop

A resend that lands right back in the dead-letter queue is an infinite loop waiting to happen. Auto-remediation runs dry-run first and carries a retry-ceiling guard so a fix that doesn't work stops itself instead of hammering a downstream that's still down.

Don't fly blind

The worst dead-letter is the one nobody sees for a week. Alert on arrival and on backlog age, and let the "new failure cluster" alert tell you the first time something breaks in a way it never has before - so a genuinely new problem stands out from the usual volume.

See it on your own dead-letters.

Connect a namespace and DLQ Manager reads the real reason, the delivery pattern and - when you link your telemetry - the consumer's own exception. Free for 30 days.