Dead Letter Queue
Route failed messages to a dead letter queue after max retries
Which to use
This page is the task-oriented walkthrough: send a message with a DLQ policy, reject it until it exhausts retries, and consume the diverted message from the DLQ. For the QueueMessagePolicy field reference — maxReceiveCount/maxReceiveQueue defaults and 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 inside QueueMessagePolicy: maxReceiveCount and maxReceiveQueue. 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 - Kotlin SDK installed (
implementation("io.kubemq.sdk:kubemq-sdk-kotlin:1.0.1"))
Code
package io.kubemq.sdk.examples.queues
import io.kubemq.sdk.client.KubeMQClient
import io.kubemq.sdk.queues.QueueMessagePolicy
import io.kubemq.sdk.queues.queueMessage
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
private const val ADDRESS = "localhost:50000"
private const val CLIENT_ID = "kotlin-queues-dead-letter"
private const val CHANNEL = "kotlin-queues.dead-letter"
private const val DLQ_CHANNEL = "kotlin-queues.dead-letter-dlq"
fun main() = runBlocking {
val client = KubeMQClient.queues {
address = ADDRESS
clientId = CLIENT_ID
}
client.use {
// Send a message with DLQ policy (max 3 receives, then route to DLQ)
println("Sending message with DLQ policy (max 3 receives)...")
client.sendQueuesMessage(queueMessage {
channel = CHANNEL
body = "DLQ after 3 nacks".toByteArray()
policy = QueueMessagePolicy(
maxReceiveCount = 3,
maxReceiveQueue = DLQ_CHANNEL,
)
})
// Reject the message 3 times
repeat(3) { attempt ->
delay(500)
val resp = client.receiveQueuesMessages {
channel = CHANNEL
maxItems = 1
waitTimeoutMs = 3000
autoAck = false
}
resp.messages.forEach { msg ->
msg.reject()
println("Rejected attempt #${attempt + 1}: ${String(msg.body)}")
}
}
// Check the DLQ
delay(1000)
val dlqResp = client.receiveQueuesMessages {
channel = DLQ_CHANNEL
maxItems = 1
waitTimeoutMs = 3000
autoAck = true
}
dlqResp.messages.forEach { msg ->
println("DLQ received: ${String(msg.body)}")
}
println("Done.")
}
}How It Works
QueueMessagePolicy(maxReceiveCount = 3, maxReceiveQueue = DLQ_CHANNEL)configures the DLQ policy.- After 3 rejections, the message is automatically moved to the DLQ channel.
- The DLQ channel can be consumed separately for manual review or reprocessing.
- This prevents poison messages from blocking the main queue indefinitely.
Related
- Dead Letter Policy — field-level reference for
maxReceiveCountandmaxReceiveQueue - Ack & Reject
- Queues Reference
Was this page helpful?