Dead letter queues
Where a broker puts messages it has given up on. On RabbitMQ the reasons are broader than failed processing — expiry and queue limits send messages there too — so the contents are a mix of bugs and capacity problems, and the list differs by broker.
Think of the shelf at a sorting office where undeliverable post ends up. Some of it is addressed wrongly. Some of it sat too long. Some arrived when the van was already full. The shelf does not care which — it is just where things go when nothing else can be done with them.
The name suggests a single meaning — messages that failed — and that reading is what makes dead letter queues confusing to operate. A broker sends messages there for several unrelated reasons, most of which are not a consumer rejecting anything, and treating the queue as a uniform pile of failures leads directly to the wrong remedy applied in bulk.
Four ways in, only one of them a bug#
RabbitMQ enumerates its triggers, and the list is more varied than the name implies. A message is dead-lettered when a consumer negatively acknowledges it with the requeue parameter set to false; when it expires through a per-message time-to-live; when it is dropped because its queue exceeded a length limit; or when it has been returned to a quorum queue more times than the delivery limit permits.
Only the first of those is code declining to process something. The second is a message that was never picked up in time. The third is a queue that filled up, which is a capacity signal about the whole system rather than a statement about the message at all — the message was fine and simply arrived when there was no room. The fourth sits between the two: something kept failing and the broker stopped counting. Four different situations, one destination, and no way to tell them apart without looking at why each message arrived.
Two things about that list are worth holding on to. It is one broker’s list, not a definition: the triggers are RabbitMQ’s, other brokers dead-letter on their own conditions, and some systems have no broker-side concept of a dead letter queue at all, only a topic that consumer code writes to by convention. So the transferable claim is not the four entries — it is that your broker has a list, that it is longer than “the handler failed”, and that you should go and read it. And the second: telling them apart is not guesswork, because the broker writes down which one applied.
The queue is a diagnosis, not a fix#
RabbitMQ records it in an x-death header on the message itself, and the fields are exactly the ones the previous section says you need. A reason, whose value is one of rejected, expired, maxlen or delivery_limit — the four triggers, named. The queue it was dead-lettered from, the exchange it had been published to and its routing-keys. A count of how many times this has happened to this message from that queue for that reason, and the time it first did. Entries are ordered by recency, most recent first, so a message that has been round the houses carries its own history in order. Any triage worth automating reads that header first and branches on reason; anything that treats the queue as an undifferentiated pile is discarding the evidence the broker already gathered.
Because the reasons differ, the remedies differ, and the instinct to write a job that replays the whole queue on a schedule is the wrong shape for at least two of the four. A message dead-lettered by a length limit usually wants replaying, provided the handler treats a repeated delivery as routine — nothing was wrong with it. A message rejected because it is malformed will fail again on the next attempt and every attempt after that, and a timer that keeps feeding it back is a machine for burning consumer capacity on a message that can never succeed.
This is also why configuration is per queue rather than global. RabbitMQ attaches the dead letter exchange to each queue through a policy or a queue argument, and recommends policies over hardcoded arguments for a specific operational reason: arguments baked into the queue declaration require an application redeployment to change, while a policy can be updated on a running system. That distinction matters most in exactly the situation dead letter queues exist for — the middle of an incident, when the routing needs changing and a deploy is the slowest available tool.
Cycles, and what stops them#
Nothing in the topology prevents a dead letter route from leading, eventually, back to the queue it came from. RabbitMQ says so plainly — it is possible to form a cycle of message dead-lettering where the same message reaches the same queue twice — and then describes the guard: it detects a cycle and drops the message, provided there was no rejection in the entire cycle.
That condition is the part worth reading twice, because it draws the boundary of the protection. A loop driven by expiry, where messages keep timing out and being re-routed, is caught and broken. A loop where a consumer is actively rejecting at some point in the ring is not — the broker treats an explicit rejection as a real decision by real code, and declines to discard a message on the strength of it. So the automatic protection covers the accidental cycle and leaves the one your own retry logic built, which is the one more likely to exist. A rejection that always happens is not a transient failure, and the design question it raises is whether this work belonged behind a broker at all.
IF YOU REMEMBER ONE THING
Read why each message arrived before deciding what to do with any of them. Expiry, a full queue and a rejection all land in the same place and want three different responses.
Questions people also ask
4 QUESTIONSWhat actually sends a message to the dead letter queue?
More than failed processing. RabbitMQ lists four triggers: the message is rejected with requeue set to false, it expires through a per-message TTL, it is dropped because the queue exceeded a length limit, or it exceeded a delivery limit on a quorum queue. Only the first is a consumer saying no — the other three are the broker enforcing limits, which is why the queue's contents need reading before they are interpreted.
How do I tell why a message was dead-lettered?
On RabbitMQ, read the x-death header the broker attaches to the message. It carries a reason field whose value is one of rejected, expired, maxlen or delivery_limit, alongside the queue it came from, the exchange and routing keys it was published with, a count of how many times this has happened for that reason, and the time it first did. Entries are ordered most recent first. That header is what turns a pile of messages into a queue you can triage by cause.
Should I automatically retry everything in the dead letter queue?
Not without knowing why each message arrived. Replaying a message that failed because of a transient outage is right; replaying one that fails deterministically on its own contents just sends it straight back, and doing that on a timer builds a loop that consumes capacity forever. The reason for arrival decides the remedy, and the reasons are not uniform.
Can a message be dead-lettered forever in a loop?
The topology allows it and the broker guards against it. RabbitMQ notes that it is possible to form a cycle where the same message reaches the same queue twice, and that it detects a cycle and drops the message where there was no rejection anywhere in that cycle. Note the condition: the protection covers cycles driven by expiry, not ones where a consumer keeps actively rejecting.