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



<Callout type="info" title="Which to use">
  This page is the field-level reference for `max_receive_count`/`max_receive_queue`, shown here via the queue-stream API. For the end-to-end task — sending a message, exhausting retries, and consuming from the resulting DLQ — see [Dead Letter Queue](./dead-letter-queue).
</Callout>

## Overview [#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 `max_receive_count`, automatically moves it to the dead-letter channel you name with `max_receive_queue`. 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 `async_nack()`, 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 [#prerequisites]

* KubeMQ server running on `localhost:50000`
* Python SDK installed (`pip install kubemq`)

## Code [#code]

```python title="dead_letter_policy.py"
"""Example: Dead letter policy — messages move to DLQ after max attempts via stream."""

from __future__ import annotations

import asyncio

from kubemq import AsyncQueuesClient
from kubemq import QueueMessage


async def main() -> None:
    async with AsyncQueuesClient(
        address="localhost:50000",
        client_id="python-queues-stream-dead-letter-policy-client",
    ) as client:
        # Send a message with combined policies: expiration + DLQ
        result = await client.send_queue_message(
            QueueMessage(
                channel="python-queues-stream.dead-letter-policy",
                body=b"message with policies",
                metadata="policy-test",
                expiration_in_seconds=60,
                delay_in_seconds=0,
                max_receive_count=3,
                max_receive_queue="python-queues-stream.dead-letter-policy-dlq",
            )
        )
        print(f"Sent with policies: {result}")
        print("  Expiration: 60s, Max attempts: 3, DLQ: dead-letter-policy-dlq")

        # Reject the message repeatedly to trigger DLQ
        for attempt in range(3):
            response = await client.receive_queue_messages(
                channel="python-queues-stream.dead-letter-policy",
                max_messages=1,
                wait_timeout_seconds=5,
            )
            if not response.messages:
                print(f"  Attempt {attempt + 1}: No message available")
                break
            for msg in response.messages:
                print(
                    f"  Attempt {attempt + 1}: receive_count={msg.receive_count}, rejecting..."
                )
                await msg.async_nack()

        # Check the DLQ
        dlq = await client.receive_queue_messages(
            channel="python-queues-stream.dead-letter-policy-dlq",
            max_messages=1,
            wait_timeout_seconds=5,
            auto_ack=True,
        )
        if dlq.messages:
            print(f"DLQ received: {dlq.messages[0].body.decode('utf-8')}")
        else:
            print("DLQ: no messages yet")


if __name__ == "__main__":
    asyncio.run(main())

```

## How It Works [#how-it-works]

* The message is sent with three combined policies: `expiration_in_seconds=60` (TTL), `max_receive_count=3` (max attempts), and `max_receive_queue` (DLQ target).
* Each `async_nack()` increments `receive_count`; after 3 rejections the broker routes the message to the DLQ channel instead of returning it.
* `expiration_in_seconds` is an independent TTL; if the message is not delivered within 60 seconds it expires whether or not it has been received.
* The DLQ channel is a standard queue; a separate consumer can drain it for debugging, alerting, or reprocessing workflows.

## Related [#related]

* [Dead Letter Queue](./dead-letter-queue) — task-oriented walkthrough for DLQ routing
* [Pattern overview](/learn/queues/getting-started)
* [Python SDK Reference](/sdks/python/reference/queues)
* [Send & Receive](/sdks/python/tutorials/send-receive)
* [Ack All](/sdks/python/how-to/queues/ack-all)
