Skip to content

TTL & Limits

If producers outpace consumers, a queue grows without bound until it eats the broker’s memory or disk — and then everything grinds to a halt. TTL and length limits are the guardrails that put a ceiling on that growth and let you decide what to drop instead of falling over.

TTL (time-to-live) makes messages expire after a set number of milliseconds. There are two ways to set it:

  • Per-queue (x-message-ttl) — every message in the queue expires after N ms.
  • Per-message (expiration property) — this specific message expires after N ms.

When a message expires it’s removed from the queue. Crucially, if the queue has a dead-letter exchange, the expired message is dead-lettered rather than silently dropped — which is the whole basis of the delayed-retry trick below.

Queue TTL (x-expires) is different: it deletes the entire queue after it has gone unused (no consumers, no gets) for N ms. Handy for cleaning up temporary queues.

x-max-length caps the number of messages (or x-max-length-bytes the total size). When the queue is full and a new message arrives, the overflow behaviour decides what gives:

  • drop-head (default) — drop the oldest message to make room for the new one.
  • reject-publish — reject the new message (the publisher can be told, via publisher confirms).

Choosing between them is a real decision: drop-head favours fresh data (good for live metrics), reject-publish favours not losing anything and pushing back on the producer (good for work you must not drop).

Declaring a queue with TTL and a length cap

Section titled “Declaring a queue with TTL and a length cap”
await channel.assertQueue('events', {
durable: true,
arguments: {
'x-message-ttl': 60000, // messages expire after 60s
'x-max-length': 10000, // keep at most 10k messages
'x-overflow': 'reject-publish', // reject new ones when full
},
});

Here’s why TTL matters beyond garbage collection. Put a TTL on a queue with no consumer, and point its dead-letter exchange back at your main queue. A message sent there sits for the TTL, expires, and gets dead-lettered back into the main flow — a delay with no scheduler.

flowchart LR
  main["main queue"] -->|"failed, send to retry"| retry["retry queue
(TTL 30s, no consumer)"]
  retry -->|"TTL expires → dead-letter"| main
  main --> worker["consumer"]
A retry queue: TTL expiry dead-letters back to the main queue

This is the standard RabbitMQ way to do retry-with-backoff, and the Reliability and Consumer Patterns modules build directly on it.

What happens to a message when its TTL expires, if the queue has a dead-letter exchange?
What does x-max-length with the default drop-head overflow do when the queue is full?
You must not lose any messages and want to push back on producers when the queue is full. Which overflow mode?
How do you build delayed retry-with-backoff in RabbitMQ using TTL?