KubeMQ
Client SDKsJavaHow-to guidesQueues

Dead Letter Queue

Route repeatedly failed KubeMQ Queue messages to a dead-letter queue with the Java SDK for later inspection.

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.

Overview

A 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

  • 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)

Code

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

  • 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.

Was this page helpful?

On this page