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