Dead-Lettering — Cheat Sheet
RabbitMQ · 4 topics. Download the PDF or the Instagram carousel and share it.
Dead Letter Exchange (DLX)
Rejected, nacked, or expired messages are routed to a DLX; configure x-dead-letter-exchange on the source queue. DLQs are essential for failure inspection and retry workflows.
- ✓A DLX is a regular exchange; configure it on the source queue via x-dead-letter-exchange argument.
- ✓Messages dead-letter for three reasons: nacked with requeue=false, TTL expired, or queue overflow (reject-publish policy).
- ✓Always monitor DLQ depth — a growing DLQ indicates a broken consumer or a schema/data problem.
- ✓The x-death header tracks how many times a message has been dead-lettered and the reason — use it for retry-count logic.
- ✓Retry-with-backoff pattern: nack → retry queue with TTL → expires back to source queue → after N retries → permanent DLQ.
- ✓In Spring AMQP, use QueueBuilder.withArgument("x-dead-letter-exchange", "dlx-name") to configure the DLX on the source queue.
Channel channel = connection.createChannel();
// 1. Declare the DLX (a regular direct exchange)
channel.exchangeDeclare("orders.dlx", BuiltinExchangeType.DIRECT, true);
// 2. Declare the DLQ
channel.queueDeclare("orders.dlq", true, false, false, null);
// 3. Bind DLQ to DLX
channel.queueBind("orders.dlq", "orders.dlx", "order-queue");
// 4. Declare the source queue with DLX argument
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "orders.dlx");
args.put("x-dead-letter-routing-key", "order-queue"); // optional override
args.put("x-message-ttl", 60_000); // 60 s TTL — expired msgs → DLX
channel.queueDeclare("order-queue", true, false, false, args);
// Now: nack with requeue=false → message goes to orders.dlq
// Or: message not consumed within 60s → goes to orders.dlqDead Letter Queues (DLQ)
Messages that cannot be processed land in a DLQ; monitor DLQ depth as an SLA signal and build tooling to replay, inspect, or discard messages after investigation.
- ✓Messages are dead-lettered on: basicNack(requeue=false), TTL expiry, or queue length overflow
- ✓DLX + DLQ must be explicitly configured — set x-dead-letter-exchange on the source queue
- ✓x-death header on dead-lettered messages contains reason, count, and origin queue
- ✓DLQ depth is a critical SLA metric — alert immediately when DLQ becomes non-empty
- ✓RejectAndDontRequeueRecoverer + Spring Retry auto-routes to DLQ after exhausting retries
- ✓Build replay tooling: re-publish DLQ messages to the original exchange for re-processing
@Configuration
class DlqConfig {
// DLX — a direct exchange that routes to the DLQ
@Bean
DirectExchange deadLetterExchange() {
return new DirectExchange("orders.dlx");
}
// DLQ — the queue that holds dead-lettered messages
@Bean
Queue deadLetterQueue() {
return QueueBuilder.durable("orders.dlq").build();
}
@Bean
Binding dlqBinding() {
return BindingBuilder.bind(deadLetterQueue())
.to(deadLetterExchange())
.with("orders.dead"); // routing key
}
// Source queue — messages here get dead-lettered to DLX
@Bean
Queue ordersQueue() {
return QueueBuilder.durable("orders.processing")
.withArgument("x-dead-letter-exchange", "orders.dlx")
.withArgument("x-dead-letter-routing-key", "orders.dead")
.withArgument("x-message-ttl", 300_000) // 5min TTL
.build();
}
}Message TTL
Set x-message-ttl (queue-level) or the expiration property (per-message) to expire messages not consumed within the specified milliseconds; expired messages are dead-lettered.
- ✓x-message-ttl (queue declaration) applies a uniform TTL to all messages in that queue
- ✓expiration (per-message property, set by producer) allows different TTLs per message
- ✓Queue-level TTL only expires messages at the queue HEAD — not mid-queue
- ✓Expired messages are dead-lettered to x-dead-letter-exchange or silently dropped
- ✓TTL + DLX enables retry-with-delay: wait queues with TTL automatically re-route after expiry
- ✓For exponential backoff: use multiple wait queues (10s, 30s, 60s) and route based on x-death count
@Bean
Queue ordersQueue() {
return QueueBuilder.durable("orders.processing")
.withArgument("x-message-ttl", 60_000) // 60 seconds
.withArgument("x-dead-letter-exchange", "orders.dlx")
.build();
}
// Or via RabbitAdmin (runtime)
rabbitAdmin.declareQueue(new Queue("orders.processing", true, false, false,
Map.of("x-message-ttl", 60_000,
"x-dead-letter-exchange", "orders.dlx")));
// Important: queue-level TTL checks messages at the QUEUE HEAD
// Messages buried mid-queue are NOT expired until they reach the head
// (Per-message TTL can expire messages anywhere in the queue — checked on delivery)Queue TTL & Auto-Expiry
x-expires deletes an unused queue after a given idle period; useful for temporary reply queues and ephemeral work queues that should not accumulate indefinitely.
- ✓x-message-ttl sets per-queue default TTL for messages in milliseconds.
- ✓Individual messages can override TTL via expiration property (string of ms).
- ✓When both queue TTL and per-message expiration are set, the lower value wins.
- ✓x-expires deletes the entire queue after an idle period — great for temporary reply queues.
- ✓Expired messages are dead-lettered to the x-dead-letter-exchange if configured, otherwise dropped.
- ✓TTL + DLX enables delay/retry patterns without requiring the delayed-message plugin.
// Queue-level TTL — all messages expire after 60 seconds
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("orders.processing")
.withArgument("x-message-ttl", 60_000) // 60 000 ms = 60 s
.withArgument("x-dead-letter-exchange", "orders.dlx") // where expired messages go
.withArgument("x-dead-letter-routing-key", "orders.expired")
.build();
}
// Per-message TTL — override on individual messages
rabbitTemplate.convertAndSend("orders", "order.placed", event, msg -> {
msg.getMessageProperties().setExpiration("30000"); // 30 s (string, not int)
return msg;
});
// Note: when BOTH queue TTL and per-message expiration are set,
// the LOWER of the two values wins (whichever expires first)