Dead Letter Policy
Configure a dead-letter policy on KubeMQ stream queues with the Python SDK to handle failed messages.
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.
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
- KubeMQ server running on
localhost:50000 - Python SDK installed (
pip install kubemq)
Code
"""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
- The message is sent with three combined policies:
expiration_in_seconds=60(TTL),max_receive_count=3(max attempts), andmax_receive_queue(DLQ target). - Each
async_nack()incrementsreceive_count; after 3 rejections the broker routes the message to the DLQ channel instead of returning it. expiration_in_secondsis 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
- Dead Letter Queue — task-oriented walkthrough for DLQ routing
- Pattern overview
- Python SDK Reference
- Send & Receive
- Ack All
Was this page helpful?