Skip to content

// Resilience & Messaging · 07

Dead-Letter Queue

Message keeps failingleads toParked for inspection

Where messages go after too many failed attempts, to be looked at.

How it fits together

Dead-Letter Queue structureOrders queuemax deliveries 5Order consumerfails on bad payload«client»Ops engineerinspect, resubmitDead-letter queuereason + body«external»AlertDLQ > 0 for 10m1 deliver2 retry x53 after 5th4 alert5 inspect6 resubmit
publishes / notifiesfails / error pathuses / callscreates· numbered steps replay in order
  • Orders queue publishes / notifies Order consumer (1 deliver)
  • Order consumer fails / error path Orders queue (2 retry x5)
  • Order consumer fails / error path Dead-letter queue (3 after 5th)
  • Dead-letter queue publishes / notifies Alert (4 alert)
  • Ops engineer uses / calls Dead-letter queue (5 inspect)
  • Ops engineer creates Orders queue (6 resubmit)

Spot it

  • One bad message blocks the whole queue
  • The same message fails in a loop for hours
  • Messages vanish after the retry limit

It costs

A DLQ only helps if someone watches it and can replay what lands there.

In code

public sealed class OrderConsumer(IOrderStore orders, IDeadLetterSink deadLetters){    private const int MaxDeliveries = 5;​    public async Task HandleAsync(Delivery delivery, CancellationToken ct)    {        try        {            var order = OrderMessage.Parse(delivery.Body);            await orders.SaveAsync(order, ct);            await delivery.CompleteAsync(ct);        }        catch (ValidationException ex)        {            await deadLetters.SendAsync(delivery, $"invalid payload: {ex.Message}", ct);        }        catch (Exception ex) when (delivery.DeliveryCount >= MaxDeliveries)        {            await deadLetters.SendAsync(delivery, $"max deliveries reached: {ex.Message}", ct);        }        catch (Exception)        {            await delivery.AbandonAsync(ct); // back on the queue for another try        }    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

A poison message fails every time: malformed payload, a bug, missing reference data. Without a limit it is redelivered forever, burns capacity and, on ordered queues or sessions, blocks everything behind it. Brokers move it aside after a max delivery count (Azure Service Bus defaults to 10; SQS calls it maxReceiveCount in its redrive policy).

The classic mistake is a DLQ nobody monitors, which is just a slower way to lose data. Alert on its depth, record why each message was dead-lettered, and build redrive tooling before you need it.

Separate transient from permanent failures: retry transient ones with backoff, dead-letter permanent ones at once. Redriven messages arrive a second time, so consumers must be idempotent.

Ask for it

  • poison message
  • max delivery count
  • redrive
  • dead-letter reason
  • visibility timeout

On the orders queue, set the max delivery count to 5, dead-letter validation failures immediately with a reason, alert when the dead-letter queue is non-empty for 10 minutes, and add a command that inspects and resubmits dead-lettered messages after a fix.