KubeMQ
Client SDKsJavaHow-to guidesQueues

Dead Letter Policy

Configure a dead-letter policy on a KubeMQ Queue stream with the Java SDK to route repeatedly failed messages.

Which to use

For the task-oriented how-to, see Dead Letter Queue. This page focuses on the attemptsBeforeDeadLetterQueue/deadLetterQueue policy option itself — its evaluation point and interaction with redelivery.

Overview

A dead-letter policy protects a queue from poison messages — a record that fails processing over and over because of a malformed payload, a consumer bug, or a downstream dependency that is down. Without one, that message is redelivered forever: it blocks head-of-line delivery, burns your consumers' retry budget, and can stall an entire queue behind a single bad record.

With a policy attached, KubeMQ counts each failed delivery and, once the message crosses attemptsBeforeDeadLetterQueue, automatically moves it to the dead-letter channel you name with deadLetterQueue. The main queue keeps flowing while the failure is quarantined for inspection or replay.

Gotchas: the receive count increments on every failed delivery — an explicit reject(), an expired transaction, or a visibility timeout — not just deliberate rejections, so set the ceiling above your normal retry budget. The dead-letter channel is an ordinary queue with no special behavior: nothing drains it for you, so monitor it and build a reprocessing path or failures pile up silently. The policy is set at send time and travels with the message, so the producer, not the consumer, decides the retry ceiling.

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

DeadLetterPolicyExample.java
package io.kubemq.example.queuesstream;

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

/**
 * DeadLetterPolicyExample for Queues Stream
 */
public class DeadLetterPolicyExample {
    private static final String ADDRESS = "localhost:50000";
    private static final String CLIENT_ID = "java-queues-dead-letter-policy-client";
    private static final String CHANNEL = "java-queues.dead-letter-policy";

    private static final String DLQ = "java-queues.dead-letter-policy-dlq";

    /**
     * Demonstrates configuring dead letter policy with max attempts
     * and automatic routing to a dead letter queue.
     */
    public static void main(String[] args) throws InterruptedException {
        // Create a 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);

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

            // Reject the message twice; on third attempt it should be in 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...");
                    resp.getMessages().get(0).reject();
                } else {
                    System.out.println("Attempt " + attempt + ": No message (moved to DLQ).");
                    break;
                }
                Thread.sleep(500);
            }

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

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

How It Works

  • attemptsBeforeDeadLetterQueue(2) on the QueueMessage sets a per-message retry cap; after two reject() calls the broker moves the message to the channel in deadLetterQueue(DLQ).
  • The retry loop runs three iterations: the first two reject the message; on the third iteration the main queue is empty (message already in DLQ).
  • The DLQ is a regular queue channel — it is polled and read the same way as any other queue; autoAckMessages(true) avoids leaving an un-acked message in the DLQ.

Was this page helpful?

On this page