KubeMQ
Client SDKsKotlinHow-to guidesQueues

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

DeadLetterQueueExample.kt
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.

Was this page helpful?

On this page