Message TTL
IntermediateSet 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.
Overview
Message TTL (Time-To-Live) controls how long a message can sit in a queue before it is discarded (or dead-lettered). There are two scopes: **queue-level TTL** (`x-message-ttl` argument on queue declaration) — all messages in the queue share the same expiry; and **per-message TTL** (`expiration` AMQP property) — each message can have its own expiry, set by the producer. Both are in milliseconds. When a message expires, it is either dropped silently or routed to the configured DLX. TTL + DLX is the building block for **retry-with-delay** patterns: dead-letter an expired message from a holding queue back to the original processing queue after a fixed delay.
Queue-Level TTL with x-message-ttl
Set `x-message-ttl` on the queue declaration. Every message in this queue expires after the specified milliseconds if not consumed. Expired messages are dead-lettered to the DLX or dropped if no DLX is configured. Queue-level TTL only expires messages at the head of the queue — messages are not checked mid-queue.
@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)Per-Message TTL via expiration Property
`expiration` is set by the producer as an AMQP message property (as a string of milliseconds). Per-message TTL is checked when the message is about to be delivered — a message with a shorter TTL that is buried behind longer-TTL messages may not expire until it reaches the front of the queue. For reliable per-message expiry, consider queue-level TTL instead.
// Per-message TTL in Java AMQP client
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.expiration("30000") // 30 seconds, as string
.deliveryMode(2) // persistent
.build();
channel.basicPublish("orders.exchange", "order.created", props, body);
// Spring AMQP: set via MessagePostProcessor
rabbitTemplate.convertAndSend("orders.exchange", "order.created", order, msg -> {
msg.getMessageProperties().setExpiration("30000");
return msg;
});
// @RabbitListener producer side with MessageProperties
@Bean
public MessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
// Priority message with short TTL (urgent, expires fast if not consumed)
rabbitTemplate.convertAndSend("orders.exchange", "order.urgent", urgentOrder, msg -> {
msg.getMessageProperties().setExpiration("5000"); // 5s — if not consumed, dead-letter
msg.getMessageProperties().setPriority(10);
return msg;
});Retry with Delay Pattern Using TTL + DLX
A common pattern for delayed retries: reject a failed message (dead-letter it) to a "wait queue" with a `x-message-ttl` of the desired delay and `x-dead-letter-exchange` pointing back to the original exchange. After the TTL expires, the message is automatically re-routed to the original processing queue — implementing retry-with-backoff without a plugin.
// Retry-with-delay via TTL + DLX:
//
// Processing queue → (on failure, basicNack requeue=false) →
// DLX → wait.queue (TTL=10s, DLX=original exchange) →
// (after 10s) → original exchange → processing queue
//
// This creates a 10-second retry delay without any application timer
@Bean
Queue waitQueue() {
return QueueBuilder.durable("orders.wait.10s")
.withArgument("x-message-ttl", 10_000) // 10s delay
.withArgument("x-dead-letter-exchange", "orders.topic") // back to source
.withArgument("x-dead-letter-routing-key", "order.created")
.build();
}
// Tip: create multiple wait queues for exponential backoff:
// orders.wait.10s (x-message-ttl=10000)
// orders.wait.30s (x-message-ttl=30000)
// orders.wait.60s (x-message-ttl=60000)
// Track retry count via x-death header and route to the appropriate wait queueKey Points to Remember
- 1x-message-ttl (queue declaration) applies a uniform TTL to all messages in that queue
- 2expiration (per-message property, set by producer) allows different TTLs per message
- 3Queue-level TTL only expires messages at the queue HEAD — not mid-queue
- 4Expired messages are dead-lettered to x-dead-letter-exchange or silently dropped
- 5TTL + DLX enables retry-with-delay: wait queues with TTL automatically re-route after expiry
- 6For exponential backoff: use multiple wait queues (10s, 30s, 60s) and route based on x-death count
Interview Questions
Sign in to ask AriaWhat is the difference between queue-level TTL and per-message TTL in RabbitMQ?
What happens to a message when it expires and no DLX is configured?
How would you implement a 30-second retry delay using TTL and DLX without a plugin?
Why may a per-message TTL not expire a message exactly at its TTL time?
How would you implement exponential backoff retries in RabbitMQ?
Ask Aria about Message TTL
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.