Setting Time-to-Live on Queues and Messages
RabbitMQ supports two ways to expire messages before they're ever consumed. A queue-level TTL, set via the x-message-ttl argument at queue declaration, applies uniformly to every message that arrives in that queue, counting from the moment each message is enqueued. A per-message TTL, set via the expiration field in message properties (as a string, in milliseconds) lets a single publisher vary the lifetime of individual messages within the same queue, for example giving a price-quote update a much shorter shelf life than an order confirmation flowing through the identical exchange and queue.
Cricket analogy: It is like a stadium setting a blanket gate-closure time for every ticket holder (queue TTL) versus a scalper printing individual tickets with different validity windows for different matches in the same series (per-message TTL).
How Expiration Actually Happens
A message doesn't expire the instant its TTL timer hits zero somewhere in the background; RabbitMQ checks expiration lazily, typically when the message reaches the head of the queue and is about to be delivered or when the queue is otherwise inspected. This means a message can technically remain in the queue past its nominal TTL if it's stuck behind older, still-unconsumed messages, since classic queues only guarantee FIFO expiry checking at the head. If the queue also has a DLX configured, an expired message is dead-lettered rather than silently vanishing, arriving with an x-death reason of 'expired' so downstream consumers of the dead-letter queue know exactly why it never made it to the original consumer.
Cricket analogy: It is like a bowler's spell limit only being enforced when the captain actually checks the over count at the end of an over, not the instant the limit is technically reached mid-delivery — the check happens lazily at a natural boundary, not continuously.
# Queue-level TTL: every message in this queue expires 60s after enqueue
channel.queue_declare(
queue='quotes.live',
durable=True,
arguments={
'x-message-ttl': 60000,
'x-dead-letter-exchange': 'quotes.dlx',
}
)
# Per-message TTL: this specific message expires in 5s regardless of queue default
channel.basic_publish(
exchange='',
routing_key='quotes.live',
body='{"symbol": "RELIANCE", "price": 2945.10}',
properties=pika.BasicProperties(expiration='5000'),
)The expiration property must be a string, not an integer — pika.BasicProperties(expiration='5000') is correct, expiration=5000 will raise an error or be silently ignored depending on the client library. This is a common source of bugs when developers copy queue-argument style (integer milliseconds) into the message property.
Combining Queue and Message TTL
When both a queue-level TTL and a per-message TTL are present, RabbitMQ applies whichever is shorter for that specific message — a per-message TTL can only tighten the effective lifetime, never extend it beyond the queue's own limit. This makes queue TTL a useful safety ceiling: you can let individual publishers set aggressive short TTLs for latency-sensitive data while a queue-level TTL acts as a backstop ensuring nothing ever lingers indefinitely even if a publisher forgets to set expiration at all.
Cricket analogy: It is like a franchise capping every player contract at a maximum of 3 years (queue TTL) even if an individual player's negotiated deal says 5 years — the shorter of the two governing limits always wins, tightening but never extending.
Because expiration is checked lazily at the head of a classic queue, a short-TTL message stuck behind a long backlog of older messages can sit in the queue well past its nominal expiry time before RabbitMQ notices and dead-letters it. If precise, low-latency expiry matters, keep the queue shallow (small backlog) or use a dedicated queue per TTL tier rather than mixing wildly different TTLs in one deep queue.
- Queue-level TTL (x-message-ttl) applies uniformly to every message enqueued; per-message TTL (expiration property) varies per message.
- The expiration message property must be set as a string of milliseconds, not an integer.
- When both TTLs are present, RabbitMQ applies whichever is shorter for that message.
- Expiration is checked lazily, typically at the head of the queue, not the instant the timer hits zero.
- A short-TTL message can sit past its nominal expiry if stuck behind older messages in a deep classic queue.
- An expired message is dead-lettered (with x-death reason 'expired') if the queue has a DLX configured, instead of vanishing silently.
- Use a dedicated queue per TTL tier if precise expiry timing matters for latency-sensitive data.
Practice what you learned
1. What data type must the per-message expiration property use in RabbitMQ client libraries like pika?
2. If a queue has x-message-ttl set to 60000 and a specific message is published with expiration='5000', when does that message actually expire?
3. When does RabbitMQ actually check whether a message's TTL has expired?
4. What happens to an expired message if the queue has a dead letter exchange configured?
Was this page helpful?
You May Also Like
Dead Letter Exchanges
Learn how RabbitMQ reroutes rejected, expired, or overflowed messages to a dead letter exchange so failures become observable and recoverable instead of silently vanishing.
Message Durability and Persistence
Learn how RabbitMQ keeps queues and messages alive across broker restarts using durable queues, persistent delivery mode, and the trade-offs involved in guaranteeing data survives a crash.
Consumer Prefetch and QoS
Learn how RabbitMQ's basic.qos prefetch setting controls how many unacknowledged messages a consumer can hold at once, balancing throughput, fairness, and memory use across a worker pool.