Dead Letter Queues and Dead Letter Topics With Valkey / Redis based JMS
Message brokers are built to deliver data, but to them, all messages are the same. But what happens when a consumer repeatedly fails to process a message payload?
Network outages, faulty consumer logic, and malformed data (aka poison messages) can easily cause your app to enter a destructive retry storm. This wastes valuable processing resources, and silent data loss might occur if the system drops the message entirely. Distributed systems that depend on message brokers must therefore account for potential failures. However, Java developers will find little native functionality that can help.
The official Java Message Service (JMS) specification focuses on standard message delivery and acknowledgment mechanisms, but it says almost nothing about dead-letter queues (DLQs) or dead-letter topics (DLTs). Because the spec leaves failure routing ambiguous, it's up to the messaging provider to establish these safety nets. Redisson PRO, a Valkey and Redis Java client, offers a simple, yet powerful, JMS API implementation that gives developers granular, predictable control over DLQs and DLTs.
How Redisson PRO's JMS Models Dead-Lettering
Whether you're building point-to-point queues or publish/subscribe (pub/sub) topics, Redisson PRO's JMS API models dead-letter destinations as standard data structures in Redisson's Reliable Queue and Reliable Topic. This is implemented differently for point-to-point and pub/sub topics:
For point-to-point messaging, a failed message is routed to a separate
RReliableQueue, which acts as a dedicated DLQ.For pub/sub messaging, failures within a Reliable PubSub topic are routed to a dedicated
RReliablePubSubTopic, which serves as the DLT.
A message is sent to a dead-letter destination if the deliveryLimit is exhausted after repeated delivery attempts, or if the consumer explicitly rejects the message. Because DLQs and DLTs are like any other destination within the Redisson ecosystem, you consume them using standard JMS code.
Configuring a Dead-Letter Queue
Setting up a dead-letter queue in Redisson PRO is remarkably easy. Everything is handled natively through the queue's configuration object. You just define both the maximum delivery threshold and the destination queue name, as in this Java code:
QueueConfig queueConfig = QueueConfig.defaults()
.deliveryLimit(5)
.visibility(Duration.ofSeconds(60))
.deadLetterQueueName("orders-dlq");
cf.setQueueConfig("orders", queueConfig);
In this configuration, the main queue (orders) is configured to move any message that fails five times to the orders-dlq destination.
Configuring a Dead-Letter Topic Per Subscription
Now, let's take a look at how you configure a DLT per subscription. The pub/sub architecture presents a different set of failure scenarios.
For example, since multiple consumers can subscribe to the same topic, a single malformed event might be successfully processed by one subscriber while crashing another.
For this reason, Redisson configures dead-letter topics (DLTs) at the subscription level rather than at the topic level:
SubscriptionConfig subConfig = SubscriptionConfig.name("auditLog")
.deliveryLimit(20)
.deadLetterTopicName("events-dlt");
cf.setSubscriptionConfig("auditLog", subConfig);
This approach allows multiple subscribers to have their own failure tolerances and processing speeds. You might configure a real-time analytics engine to discard a failed event after a single retry, while a mission-critical financial audit log might retry 20 times before routing the payload to an events-dlt for manual investigation. A single failing consumer will never disrupt the data flow or routing logic for other subscribers in your distributed system.
Using JNDI in Your Apps? Redisson Offers Configuration Equivalents
For enterprise applications that use the Java Naming and Directory Interface (JNDI), Redisson provides the same dead-letter capabilities. Configuration is done in flat property files, making it easy to map queue and subscription settings.
Here's what these property files look like:
# Queue configuration
queue.orders.name=orders
queue.orders.deliveryLimit=5
queue.orders.deadLetterQueueName=orders-dlq
# Topic subscription configuration
topic.events.name=events
topic.events.subscription.auditLog.deliveryLimit=20
topic.events.subscription.auditLog.deadLetterTopicName=events-dlt
Mistakes to Avoid When Working With Dead-Letter Destinations
While dead-letter destinations offer a lot of benefits, there are some common pitfalls that can cause major problems in your distributed apps. This includes:
Not declaring a DLQ: Before setting deadLetterQueueName as your DLQ, ensure the destination has already been provisioned.
Setting the delivery limit too low: A limit of 1 or 2 might route messages to the DLQ during minor, transient network blips. Allow enough headroom for temporary issues to resolve on their own.
DLQ-ing transient failures: If you route transient failures to a DLQ without any replay tooling in place, your DLQ becomes a permanent message graveyard.
No alerting on DLQ depth: If a DLQ grows without triggering alerts, you are merely deferring data loss rather than preventing it.
Configuring DLTs at the topic level: You must configure DLTs per subscription. Attempting to force topic-wide failure states ignores independent offset tracking and consumer behavior.
Unbounded DLQ growth: Always apply a TTL or maximum size limit to your DLQ to protect your Valkey/Redis memory from infinite growth.
Bring Reliable Messaging to Your Stack With Redisson PRO
Managing message delivery states in distributed apps is often left to developers to handle with custom code. Redisson PRO's DLQ and DLT features ensure your systems remain fault-tolerant, isolated, and easy to debug.
To learn more about how Redisson PRO stabilizes message delivery, read about its Reliable Queue feature.