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
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)anddeadLetterQueue(DLQ_CHANNEL)are set on theQueueMessage; after two rejected deliveries the broker automatically routes the message to the DLQ channel.- Each
reject()call incrementsmsg.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
Was this page helpful?