# Dead Letter Policy (/sdks/kotlin/how-to/queues/dead-letter-policy)



<Callout type="info" title="Which to use">
  This page is the field-level reference for `QueueMessagePolicy`. For the end-to-end task — sending a message, rejecting it until it exhausts retries, and consuming from the resulting DLQ — see [Dead Letter Queue](./dead-letter-queue).
</Callout>

## Overview [#overview]

`QueueMessagePolicy` carries two fields — `maxReceiveCount` and `maxReceiveQueue` — that together define a message's dead-letter routing. The policy is attached to a message at send time and travels with it; the *producer*, not the consumer, decides the retry ceiling.

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

## Prerequisites [#prerequisites]

* KubeMQ server running on `localhost:50000`
* Kotlin SDK installed (`implementation("io.kubemq.sdk:kubemq-sdk-kotlin:1.0.1")`)

## Code [#code]

```kotlin title="DeadLetterPolicyExample.kt"
package io.kubemq.sdk.examples.queuesstream

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-policy"
private const val CHANNEL = "kotlin-queues.dead-letter-policy"
private const val DLQ = "kotlin-queues.dead-letter-policy-dlq"

fun main() = runBlocking {
    val client = KubeMQClient.queues {
        address = ADDRESS
        clientId = CLIENT_ID
    }

    client.use {
        try {
            client.createQueuesChannel(CHANNEL)
            client.createQueuesChannel(DLQ)

            // Send message with DLQ policy (moves to DLQ after 2 rejections)
            client.sendQueuesMessage(queueMessage {
                channel = CHANNEL
                body = "Poison message".toByteArray()
                policy = QueueMessagePolicy(
                    maxReceiveCount = 2,
                    maxReceiveQueue = DLQ,
                )
            })
            println("Sent message with DLQ policy (max 2 attempts).\n")

            // Reject the message; after max attempts it should go to DLQ
            for (attempt in 1..3) {
                val resp = client.receiveQueuesMessages {
                    channel = CHANNEL
                    maxItems = 1
                    waitTimeoutMs = 2000
                    autoAck = false
                }
                if (resp.messages.isNotEmpty()) {
                    println("Attempt $attempt: Rejecting...")
                    resp.messages.first().reject()
                } else {
                    println("Attempt $attempt: No message (moved to DLQ).")
                    break
                }
                delay(500)
            }

            // Read the message from the dead letter queue
            val dlqResp = client.receiveQueuesMessages {
                channel = DLQ
                maxItems = 1
                waitTimeoutMs = 2000
                autoAck = true
            }
            if (dlqResp.messages.isNotEmpty()) {
                println("\nDLQ message: ${String(dlqResp.messages.first().body)}")
            }
        } finally {
            try { client.deleteQueuesChannel(CHANNEL) } catch (_: Exception) {}
            try { client.deleteQueuesChannel(DLQ) } catch (_: Exception) {}
        }
        println("Done.")
    }
}
```

## Field reference [#field-reference]

* **`maxReceiveCount`** (`Int`) — the number of failed receives allowed before the broker reroutes the message. `maxReceiveCount = 2` means the message survives 2 rejections; the next failed receive triggers the move.
* **`maxReceiveQueue`** (`String`) — the destination channel name for diverted messages. Must be a valid, pre-existing (or auto-creatable) channel name — a typo silently creates an unrelated channel instead of failing loudly.
* Both fields live on `QueueMessagePolicy`, which is set once per message via the `policy` parameter of `queueMessage { ... }` — there is no way to change it after the message is queued.

For the walkthrough of sending, rejecting, and consuming from the resulting DLQ, see [Dead Letter Queue](./dead-letter-queue).

## Related [#related]

* [Dead Letter Queue](./dead-letter-queue) — task-oriented walkthrough for DLQ routing
* [Ack & Reject](/sdks/kotlin/how-to/queues/ack-reject)
* [Queues Reference](/sdks/kotlin/reference/queues)
