In messaging systems, not every message can be processed successfully. Some messages may be invalid, expired, or cause errors in consumers.
Instead of losing these messages, RabbitMQ provides a feature called the Dead Letter Queue (DLQ). DLQs allow you to capture, inspect, and handle failed messages safely.
By default, any message that is rejected with requeue=false or expired will go straight to the DLQ. But in real-world systems, we don’t want a message to be sent to DLQ on the first failure. Instead, we usually give the message a few retry attempts (e.g., 3 times) before moving it permanently to the DLQ.
How to Implement “Retry 3 Times Before DLQ”
There are several approaches. The simplest way:
- Add a retry counter to the message (via headers).
- If processing fails:
- Increase the counter.
- If counter < 3 → requeue the message (or publish back to main queue).
- If counter ≥ 3 → reject with
requeue=false, so RabbitMQ sends it to the DLQ.
System Illustration
flowchart TD P[Producer] --> MQ[Main Queue] MQ --> C[Consumer] C -- success --> Done[Processed OK] C -- fail < 3 --> Requeue[Retry again in Main Queue] C -- fail >= 3 --> DLX((Dead Letter Exchange)) --> DLQ[Dead Letter Queue]
Go Implementation
We’ll use the official Go client:
go get github.com/rabbitmq/amqp091-go
Step 1: Setup DLQ and Main Queue
// declare DLX and DLQ
ch.ExchangeDeclare("dlx", "direct", true, false, false, false, nil)
qDLQ, _ := ch.QueueDeclare("dead_letters", true, false, false, false, nil)
ch.QueueBind(qDLQ.Name, "failed", "dlx", false, nil)
// main queue with DLX config
args := amqp.Table{
"x-dead-letter-exchange": "dlx",
"x-dead-letter-routing-key": "failed",
}
qMain, _ := ch.QueueDeclare("main_queue", true, false, false, false, args)
Step 2: Publisher – send initial messages
body := "Task X"
ch.Publish("", qMain.Name, false, false, amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
Headers: amqp.Table{"x-retry": int32(0)}, // start retry count
})
Pages: 1 2
Category: RabbitMQ
