TTL & Limits
Queues can’t grow forever
Section titled “Queues can’t grow forever”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 — messages that expire
Section titled “TTL — messages that expire”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 (
expirationproperty) — 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.
Length limits — cap the backlog
Section titled “Length limits — cap the backlog”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 },});channel.queue_declare( queue="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 },)args := amqp.Table{ "x-message-ttl": int32(60000), // messages expire after 60s "x-max-length": int32(10000), // keep at most 10k messages "x-overflow": "reject-publish", // reject new ones when full}ch.QueueDeclare("events", true, false, false, false, args)The trick: TTL + DLX = delayed retry
Section titled “The trick: TTL + DLX = delayed retry”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"]
This is the standard RabbitMQ way to do retry-with-backoff, and the Reliability and Consumer Patterns modules build directly on it.