In RabbitMQ, when a message becomes a dead letter (a message that consumers cannot process) in a queue, it is re-routed to another exchange, which we call a dead letter exchange. The dead letter exchange then delivers the dead letter to a queue, which is the dead letter queue.

Dead Letter Queue Illustration

Dead Letter Queue

The above illustration describes the entire process from the generation of dead letters to their handling.

Generation of Dead Letters

The following are the conditions for the generation of dead letters:

  • The message is manually rejected by the consumer (basic.reject / basic.nack), and requeue = false.
  • The message time-to-live (TTL) expires.
  • The queue reaches maximum length.

Dead Letter Queue Handling Steps

  1. Define a dead letter exchange (don’t be misled by the name, it’s just a regular exchange, it’s only called that in the context of dead letter handling).
  2. Define a queue to bind to the dead letter exchange (this queue is called the dead letter queue, and it’s also a regular queue).
  3. Define a dead letter consumer to consume the dead letter queue (don’t be misled by the name, it’s also a regular consumer).
  4. Bind the dead letter exchange to the specified queue (the queue that needs to handle dead letters should be bound).

Tip: Refer to the illustration above for the principle. All programming languages handle dead letter queues in a similar manner.

Handling Dead Letter Queue in Golang

1. Define Dead Letter Exchange

Define it like a regular exchange.

// Declare the exchange
err = ch.ExchangeDeclare(
    "tizi365.dead",   // Exchange name
    "topic", // Exchange type
    true,     // Durable
    false,
    false,
    false,
    nil,
)

2. Define Dead Letter Queue

Define it like a regular queue.

    // Declare the queue
    q, err := ch.QueueDeclare(
        "",    // Queue name, leave blank to generate a random one
        false, // Durable queue
        false,
        true,
        false,
        nil,
    )

    // Bind the queue to the dead letter exchange
    err = ch.QueueBind(
        q.Name, // Queue name
        "#",     // Routing key, # means match all routing keys, meaning receive all dead letter messages
        "tizi365.dead", // Dead letter exchange name
        false,
        nil)

Tip: Treat the dead letter queue as a regular queue.

3. Define Dead Letter Consumer

// Create a consumer
msgs, err := ch.Consume(
    q.Name, // Reference the earlier dead letter queue name
    "",     // Consumer name, if not provided, a random one will be generated
    true,   // Auto-acknowledgement of message processing
    false, 
    false, 
    false, 
    nil,
)

// Loop to consume messages from the dead letter queue
for d := range msgs {
    log.Printf("Received dead letter message=%s", d.Body)
}

4. Bind the Dead Letter Exchange to a Specific Queue

    // Queue properties
    props := make(map[string]interface{})
    // Bind the dead letter exchange
    props["x-dead-letter-exchange"] = "tizi365.dead"
    // Optional: Set the routing key when dead letter is delivered to the dead letter exchange. If not set, the original message's routing key will be used.
    // props["x-dead-letter-routing-key"] = "www.tizi365.com"

    q, err := ch.QueueDeclare(
        "tizi365.demo.hello", // Queue name
        true,   // Durable
        false, 
        false, 
        false,   
        props,     // Set queue properties
    )

This way, if messages in the tizi365.demo.hello queue become dead letters, they will be forwarded to the tizi365.dead dead letter exchange.