RabbitMQ in .NET 10 Minimal APIs: Confirms, Retries and a Dead-Letter Queue
Most RabbitMQ samples stop at hello-world messaging, leaving out message loss, provider outages and permanent failures. This walkthrough builds an order endpoint on .NET 10 Minimal APIs and wires publisher confirms, a 10-second delayed retry queue and a dead-letter queue that catches messages after four failed attempts.
Key points
- Enabling publisher confirmations and tracking via CreateChannelOptions makes BasicPublishAsync wait for broker acknowledgement instead of firing and forgetting
- The retry queue has no consumer: x-message-ttl of 10,000 ms expires the message, which is dead-lettered back to the working queue
- Retry counting lives in an x-attempt header on the app side rather than relying on broker requeue semantics, capped at four attempts
- Poison messages are rejected immediately with BasicRejectAsync because retrying malformed JSON cannot succeed
- Queues are declared as quorum queues and messages are published with DeliveryMode.Persistent for durability
- One lazily created connection with AutomaticRecoveryEnabled handles reconnects, while channels are serialized behind a semaphore
The sample uses .NET 10, RabbitMQ.Client 7.2.2 and a topology of a topic exchange, quorum queues and a fanout dead-letter exchange. Publisher confirms with tracking are enabled through CreateChannelOptions, so BasicPublishAsync returns only once the broker accepts the message, and mandatory: true stops unroutable messages from vanishing quietly.
The consumer is a BackgroundService: it acknowledges successes, rejects malformed JSON outright, republishes transient failures with a bumped x-attempt header, and after four attempts rejects the message into the dead-letter queue. The retry queue has no consumer; a 10-second message TTL expires the message and dead-letters it back to the working queue. Channels are guarded by a semaphore because they are not thread-safe, and the retry copy is published before the original is acknowledged to avoid loss.
Real-world use
- An e-commerce backend that sends confirmation emails or invoices on order placement and must not lose messages during provider outages
- A support workflow where permanently failing integration messages are parked in a dead-letter queue for manual triage
- Durable event publishing across several .NET services that split work over RabbitMQ queues under load
- Enterprise integrations that need to isolate malformed or stale-schema messages without blocking the main flow
Why it matters
Without confirms, delayed retries and a dead-letter queue, a queue-backed service either loses messages silently or loops forever on a bad one; the article shows how the three fit together on RabbitMQ.Client 7.x with .NET 10.