Dead Letter Queues
INFO
The end result is the same regardless, but Wolverine bypasses this functionality to move messages to the dead letter queue in Buffered or Durable queue endpoints.
By default, Wolverine's Rabbit MQ transport supports the native dead letter exchange functionality in Rabbit MQ itself. If running completely with default behavior, Wolverine will:
- Declare a queue named
wolverine-dead-letter-queueas the system dead letter queue for the entire application -- but don't worry, that can be overridden queue by queue - Add the
x-dead-letter-exchangeargument to each non-system queue created by Wolverine in Rabbit MQ
Great, but someone will inevitably want to alter the dead letter queue functionality to use differently named queues like so:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Use a different default deal letter queue name
opts.UseRabbitMq()
.CustomizeDeadLetterQueueing(new DeadLetterQueue("error-queue"))
// or conventionally
.ConfigureListeners(l => { l.DeadLetterQueueing(new DeadLetterQueue($"{l.QueueName}-errors")); });
// Use a different dead letter queue for this specific queue
opts.ListenToRabbitQueue("incoming")
.DeadLetterQueueing(new DeadLetterQueue("incoming-errors"));
}).StartAsync();WARNING
You will need this if you are interoperating against NServiceBus!
But wait there's more! Other messaging tools or previous usages of Rabbit MQ in your environment may have already declared the Rabbit MQ queues without the x-dead-letter-exchange argument, meaning that Wolverine will not be able to declare queues for you, or might do so in a way that interferes with other messaging tools. To avoid all that hassle, you can opt out of native Rabbit MQ dead letter queues with the InteropFriendly option:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Use a different default deal letter queue name
opts.UseRabbitMq()
.CustomizeDeadLetterQueueing(
new DeadLetterQueue("error-queue", DeadLetterQueueMode.InteropFriendly))
// or conventionally
.ConfigureListeners(l =>
{
l.DeadLetterQueueing(new DeadLetterQueue($"{l.QueueName}-errors",
DeadLetterQueueMode.InteropFriendly));
});
// Use a different dead letter queue for this specific queue
opts.ListenToRabbitQueue("incoming")
.DeadLetterQueueing(new DeadLetterQueue("incoming-errors", DeadLetterQueueMode.InteropFriendly));
}).StartAsync();Enhanced Dead Lettering with Exception Metadata
By default, Wolverine uses RabbitMQ's native NACK mechanism to move failed messages to the dead letter exchange. While simple, this approach does not include any information about why the message failed.
With EnableEnhancedDeadLettering(), Wolverine will instead publish failed messages directly to the dead letter queue with exception metadata headers, then ACK the original message. This gives you structured failure information on each dead-lettered message:
| Header | Description |
|---|---|
exception-type | Full type name of the exception |
exception-message | The exception message |
exception-stack | The exception stack trace |
failed-at | Unix timestamp (milliseconds) when the failure occurred |
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseRabbitMq()
.EnableEnhancedDeadLettering();
}).StartAsync();TIP
These same metadata headers are automatically included for all other Wolverine transports (SQS, Azure Service Bus, GCP Pub/Sub, NATS, Kafka, Redis, Pulsar) when messages are moved to dead letter queues.
WARNING
Enhanced dead lettering bypasses RabbitMQ's native dead letter exchange (DLX) mechanism. Messages are published to the DLQ by Wolverine rather than being NACK'd. If you rely on native DLX routing or policies, this mode may not be appropriate.
And lastly, if you don't particularly want to have any Rabbit MQ dead letter queues and you quite like the database backed dead letter queues you get with Wolverine's message durability, you can use the WolverineStorage option:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Disable dead letter queueing by default
opts.UseRabbitMq()
.DisableDeadLetterQueueing()
// or conventionally
.ConfigureListeners(l =>
{
// Really does the same thing as the first usage
l.DisableDeadLetterQueueing();
});
// Disable the dead letter queue for this specific queue
opts.ListenToRabbitQueue("incoming").DisableDeadLetterQueueing();
}).StartAsync();Recovering Native Dead Letters to Durable Storage 6.9
With native dead lettering, failed messages land in a RabbitMQ dead letter queue and are only visible through RabbitMQ tooling. Tools that manage Wolverine's durable dead letters (for example CritterWatch) can't see or replay them.
EnableDeadLetterQueueRecovery() starts a background listener that consumes the native dead letter queue(s) and copies each message into Wolverine's durable dead letter storage (the wolverine_dead_letters table), where it becomes queryable and replayable through IDeadLetters:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Durable message storage is required — the recovered dead letters
// are written to the wolverine_dead_letters table.
opts.PersistMessagesWithPostgresql(connectionString);
opts.UseRabbitMq()
.AutoProvision()
// Consume the native dead letter queue and copy the messages into
// Wolverine's durable dead letter storage.
.EnableDeadLetterQueueRecovery();
opts.ListenToRabbitQueue("orders");
}).StartAsync();With no arguments, the default wolverine-dead-letter-queue is consumed. Pass explicit queue names to recover from custom-named dead letter queues:
opts.UseRabbitMq()
.EnableDeadLetterQueueRecovery("orders-errors", "shipments-errors");The original exception type and message are reconstructed from the RabbitMQ x-death metadata (and from the enhanced dead lettering headers when those are present).
TIP
The same EnableDeadLetterQueueRecovery() syntax is available on the Amazon SQS and Azure Service Bus transports, so "bridge my native dead letters into durable storage" is a one-call decision on every native-dead-letter transport.

