# Dead Letter Queue (/sdks/ruby/how-to/queues/dead-letter-queue)



<Callout type="info" title="Which to use">
  This page is the task-oriented walkthrough: send a message with DLQ routing configured, let it exhaust retries, and consume the diverted message. For the `QueueMessagePolicy` field reference — `max_receive_count`/`max_receive_queue` defaults and edge cases — see [Dead Letter Policy](./dead-letter-policy).
</Callout>

## Overview [#overview]

A &#x2A;*dead-letter queue (DLQ)** gives a poison message somewhere to go instead of looping through consumers forever. When a message keeps failing — a malformed payload, a downstream outage, a handler bug — retrying it forever wastes consumer cycles and blocks everything behind it. A DLQ takes that decision out of your hands: past a set number of failed attempts, the broker diverts the message to a separate channel instead of retrying it again.

Routing runs on two settings inside `QueueMessagePolicy`: `max_receive_count` and `max_receive_queue`. Every failed delivery — a `nack_all`, a reject, or an expired visibility window — increments the receive count; past the threshold, the broker reroutes the message to the DLQ instead of redelivering it. The DLQ itself is an ordinary queue, consumed like any other channel.

**Gotchas:** the DLQ doesn't drain itself — a dedicated consumer must watch it. The count increments on *any* failed delivery, not just deliberate rejections — a slow consumer that lets the visibility window lapse counts the same as an explicit nack. A typo in the DLQ channel name quietly creates an unrelated channel instead of failing loudly.

## Prerequisites [#prerequisites]

* KubeMQ server running on `localhost:50000`
* Ruby SDK installed (`gem install kubemq`)

## Code [#code]

```ruby title="main.rb"
require 'kubemq'

address = ENV.fetch('KUBEMQ_ADDRESS', 'localhost:50000')
channel = 'ruby-queues.dead-letter-queue'
dlq_channel = 'ruby-queues.dead-letter-queue-dlq'

begin
  client = KubeMQ::QueuesClient.new(address: address, client_id: 'qstream-dlq-example')
  puts "Connected to #{address}"

  policy = KubeMQ::Queues::QueueMessagePolicy.new(max_receive_count: 2, max_receive_queue: dlq_channel)
  msg = KubeMQ::Queues::QueueMessage.new(
    channel: channel,
    metadata: 'fragile-message',
    body: 'may-fail',
    policy: policy
  )
  client.send_queue_message(msg)
  puts "Sent message with max_receive_count=2, DLQ=#{dlq_channel}"

  receiver = client.create_downstream_receiver

  2.times do |attempt|
    request = KubeMQ::Queues::QueuePollRequest.new(channel: channel, max_items: 1, wait_timeout: 3)
    response = receiver.poll(request)
    puts "Attempt #{attempt + 1}: #{response.messages.size} messages"
    response.nack_all if response.messages.any?
  end

  sleep 1
  dlq_request = KubeMQ::Queues::QueuePollRequest.new(channel: dlq_channel, max_items: 1, wait_timeout: 3)
  dlq_response = receiver.poll(dlq_request)
  puts "DLQ messages: #{dlq_response.messages.size}"
  dlq_response.messages.each do |m|
    puts "  DLQ: #{m.metadata}"
    m.ack
  end
rescue KubeMQ::Error => e
  puts "KubeMQ error: #{e.message}"
ensure
  receiver&.close
  client&.close
  puts 'Done'
end
```

## How It Works [#how-it-works]

* `QueueMessagePolicy` with `max_receive_count` and `max_receive_queue` configures automatic dead-letter routing.
* After exceeding the max receive count, the broker moves the message to the dead-letter queue.
* Review timeouts, channel names, and client IDs before running against shared environments.

## Related [#related]

* [Pattern overview](/learn/queues/getting-started)
* [Ruby SDK Reference](/sdks/ruby/reference)
* [Dead Letter Policy](/sdks/ruby/how-to/queues/dead-letter-policy)
* [Ack/Reject](/sdks/ruby/how-to/queues/ack-reject)
