# Dead Letter Queue (/sdks/python/how-to/queues/dead-letter-queue)



<Callout type="info" title="Which to use">
  This page is the task-oriented walkthrough: send a message with DLQ routing configured, let it exhaust retries, and consume the diverted message. For the `max_receive_count`/`max_receive_queue` field reference — defaults and edge cases — see [Dead Letter Policy](./dead-letter-policy).
</Callout>

## Overview [#overview]

A &#x2A;*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: `max_receive_count` and `max_receive_queue`. Every failed delivery — a nack, a reject, 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 nack. A typo in the DLQ channel name quietly creates an unrelated channel instead of failing loudly.

## Prerequisites [#prerequisites]

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

## Code [#code]

```python title="dead_letter_queue.py"
"""Example: Dead letter queue — messages move to DLQ after max receive attempts."""

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-dead-letter-queue-client",
    ) as client:
        # Send a message with DLQ configuration
        await client.send_queue_message(
            QueueMessage(
                channel="python-queues.dead-letter-queue",
                body=b"message with DLQ policy",
                metadata="dlq-test",
                max_receive_count=3,
                max_receive_queue="python-queues.dead-letter-queue-dlq",
            )
        )
        print("Sent message with max 3 attempts before DLQ")

        # Simulate failed processing by rejecting the message multiple times
        for attempt in range(3):
            response = await client.receive_queue_messages(
                channel="python-queues.dead-letter-queue",
                max_messages=1,
                wait_timeout_seconds=5,
            )
            if not response.messages:
                print(f"  Attempt {attempt + 1}: No message (already moved to DLQ)")
                break
            for msg in response.messages:
                print(
                    f"  Attempt {attempt + 1}: Received (receive_count={msg.receive_count}), "
                    f"rejecting..."
                )
                await msg.async_nack()

        # Check the DLQ for the message
        dlq_response = await client.receive_queue_messages(
            channel="python-queues.dead-letter-queue-dlq",
            max_messages=1,
            wait_timeout_seconds=5,
            auto_ack=True,
        )
        print(f"DLQ messages: {len(dlq_response.messages)}")
        for msg in dlq_response.messages:
            print(f"  DLQ message: {msg.body.decode('utf-8')}")


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

```

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

* `QueueMessage` is sent with `max_receive_count=3` and `max_receive_queue` pointing to a DLQ channel; after 3 nacks the broker automatically forwards the message.
* Each `async_nack()` call increments the message's `receive_count`; when it reaches `max_receive_count`, the broker routes to `max_receive_queue` instead of returning it.
* The DLQ is a regular queue channel; messages there can be inspected or reprocessed by a dedicated consumer.
* `auto_ack=True` on the DLQ poll acknowledges the DLQ message immediately, confirming the routing worked as expected.

## Related [#related]

* [Dead Letter Policy](./dead-letter-policy) — field-level reference for `max_receive_count` and `max_receive_queue`
* [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)
