Skip to main content
Version: Next (unreleased)

Dead-Letter Queues

WithDeadLetterQueue sets up a work queue's dead-letter topology in one call — it declares the dead-letter exchange, the dead-letter queue, the binding between them, and wires the work queue to dead-letter into it. Like the rest of the topology, it is re-applied on every reconnect. Combined with the default RequeueOnError: false, a failed handler's message is captured on the DLQ instead of being requeued or discarded:

consConfig := rabbitmq.DefaultConsumerConfig().
WithQueueConfig(rabbitmq.DefaultQueueConfig("orders")).
WithDeadLetterQueue(rabbitmq.DefaultDeadLetterConfig("orders")) // orders.dlx / orders.dlq

consumer, err := rabbitmq.NewConsumer(conn, consConfig)
// ... consume "orders"; failures are dead-lettered automatically.

// Read dead-lettered messages like any other queue:
dlq, _ := rabbitmq.NewConsumer(conn,
rabbitmq.DefaultConsumerConfig().WithQueue(consumer.DeadLetterQueueName()))

DefaultDeadLetterConfig("orders") derives a durable fanout orders.dlx and a durable orders.dlq; tune names, durability, quorum, max-length, or a TTL with the With* builders on DeadLetterConfig. The work queue must have a name (it carries the dead-letter wiring).

Broker-level backoff retry

For anything but short retries, prefer BackoffRetryMiddleware over RetryMiddleware. Instead of sleeping in-process, it re-publishes a delayed copy of the failed message back to the work queue and acks the original, so the handler goroutine and prefetch slot are freed for the whole backoff — one poison message can no longer stall the consumer. The delay grows exponentially from base and the message is redelivered by the broker. After maxRetries the message is terminal: it is rejected without requeue — dead-lettered if a dead-letter exchange is configured, otherwise discarded — regardless of RequeueOnError or a handler ErrRequeue, so it can never loop forever.

pub, _ := rabbitmq.NewPublisher(conn, rabbitmq.DefaultPublisherConfig())

consConfig := rabbitmq.DefaultConsumerConfig().
WithQueue("orders").
WithDeadLetterQueue(rabbitmq.DefaultDeadLetterConfig("orders")). // exhausted retries land here
WithMiddleware(
// 1s, 2s, 4s, ... (snapped up to the delay ladder), then dead-lettered.
rabbitmq.BackoffRetryMiddleware(pub, "orders", 5, 1*time.Second),
)

queue must be a named work queue (the retry is redelivered to it by name). A handler returning ErrDrop opts out of retrying. Retrying is at-least-once — re-publishing the copy and acking the original are not atomic — so handlers should be idempotent.

See Middleware → Error Handling for the full set of sentinel errors a handler can return to override the RequeueOnError default per message.