Dead Letter Queue
Route failed KubeMQ queue messages to a dead-letter queue using the Python SDK for safe handling.
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.
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 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
- KubeMQ server running on
localhost:50000 - Python SDK installed (
pip install kubemq)
Code
"""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
QueueMessageis sent withmax_receive_count=3andmax_receive_queuepointing to a DLQ channel; after 3 nacks the broker automatically forwards the message.- Each
async_nack()call increments the message'sreceive_count; when it reachesmax_receive_count, the broker routes tomax_receive_queueinstead of returning it. - The DLQ is a regular queue channel; messages there can be inspected or reprocessed by a dedicated consumer.
auto_ack=Trueon the DLQ poll acknowledges the DLQ message immediately, confirming the routing worked as expected.
Related
- Dead Letter Policy — field-level reference for
max_receive_countandmax_receive_queue - Pattern overview
- Python SDK Reference
- Send & Receive
- Ack All
Was this page helpful?