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



<Callout type="info" title="Which to use">
  This is the task-oriented guide for routing failed messages to a dead-letter queue. For the `attemptsBeforeDeadLetterQueue`/`deadLetterQueue` policy option and its 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 attached to the message: `attemptsBeforeDeadLetterQueue()` and `deadLetterQueue()`. Every failed delivery — a `reject()`, a nack, 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 reject. A typo in the DLQ channel name quietly creates an unrelated channel instead of failing loudly.

## Prerequisites [#prerequisites]

* KubeMQ server running on `localhost:50000`
* Java SDK installed (`implementation 'io.kubemq.sdk:kubemq-sdk-Java:3.1.1'` (Gradle) or Maven dependency from [Getting Started](/sdks/java))

## Code [#code]

```java title="DeadLetterQueueExample.java"
package io.kubemq.example.queues;

import io.kubemq.sdk.queues.*;
import java.util.UUID;

/**
 * Dead Letter Queue Example
 *
 * Demonstrates DLQ routing when messages exceed maximum receive attempts.
 */
public class DeadLetterQueueExample {
    private static final String ADDRESS = "localhost:50000";
    private static final String CLIENT_ID = "java-queues-dead-letter-queue-client";
    private static final String CHANNEL = "java-queues.dead-letter-queue";
    private static final String DLQ_CHANNEL = "java-queues.dead-letter-queue-dlq";

    public static void main(String[] args) throws InterruptedException {
        // Create a queues client connected to the KubeMQ server
        try (QueuesClient client = QueuesClient.builder().address(ADDRESS).clientId(CLIENT_ID).build()) {
            // Create main queue and dead letter queue channels
            client.createQueuesChannel(CHANNEL);
            client.createQueuesChannel(DLQ_CHANNEL);

            // Send message with DLQ config (routed to DLQ after max attempts)
            client.sendQueueMessage(QueueMessage.builder()
                    .id(UUID.randomUUID().toString()).channel(CHANNEL)
                    .body("Message with DLQ".getBytes())
                    .attemptsBeforeDeadLetterQueue(2).deadLetterQueue(DLQ_CHANNEL).build());
            System.out.println("Sent message with DLQ config (max 2 attempts).\n");

            // Receive and reject repeatedly until message moves to DLQ
            for (int attempt = 1; attempt <= 3; attempt++) {
                QueuesPollResponse resp = client.receiveQueueMessages(QueuesPollRequest.builder()
                        .channel(CHANNEL).pollMaxMessages(1).pollWaitTimeoutInSeconds(2).build());
                if (!resp.getMessages().isEmpty()) {
                    System.out.println("Attempt " + attempt + ": Rejecting (receiveCount=" + resp.getMessages().get(0).getReceiveCount() + ")");
                    resp.getMessages().get(0).reject();
                } else {
                    System.out.println("Attempt " + attempt + ": No message in main queue.");
                    break;
                }
                Thread.sleep(500);
            }

            // Receive from the dead letter queue (message routed after max rejections)
            System.out.println("\nChecking DLQ...");
            QueuesPollResponse dlqResp = client.receiveQueueMessages(QueuesPollRequest.builder()
                    .channel(DLQ_CHANNEL).pollMaxMessages(1).pollWaitTimeoutInSeconds(2).autoAckMessages(true).build());
            if (!dlqResp.getMessages().isEmpty()) {
                System.out.println("Found in DLQ: " + new String(dlqResp.getMessages().get(0).getBody()));
            }

            // Clean up resources
            client.deleteQueuesChannel(CHANNEL);
            client.deleteQueuesChannel(DLQ_CHANNEL);
        }
    }
}

```

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

* `attemptsBeforeDeadLetterQueue(2)` and `deadLetterQueue(DLQ_CHANNEL)` are set on the `QueueMessage`; after two rejected deliveries the broker automatically routes the message to the DLQ channel.
* Each `reject()` call increments `msg.getReceiveCount()` on the server side; on the third poll the main queue is empty and the message has moved to the DLQ.
* The DLQ poll uses `autoAckMessages(true)` to confirm receipt without writing additional ack logic, which is typical for DLQ monitoring flows.

## Related [#related]

* [Pattern overview](/learn/queues/getting-started)
* [Java SDK Reference](/sdks/java/reference/queues)
* [Send & Receive](/sdks/java/tutorials/send-receive)
* [Ack All](/sdks/java/how-to/queues/ack-all)
