Message queues
Message queues: retries, dead letters, and duplicates
Patterns to keep a failed message from dragging down the rest of the system
When a consumer fails, the queue can become an error amplifier if there is no clear retry policy. We saw this several times in production: a service that responds slowly because of an external dependency starts returning timeouts, the broker requeues the messages, the consumer becomes saturated, and within minutes the queue grows faster than any worker can process. The problem is not the original failure, it is the absence of a plan for when something fails.
This text compares backoff approaches, dead-letter queues, and deduplication by idempotency key, with configuration examples in well-known brokers. The idea is simple: every message that cannot be processed must have a destination decided in advance, not improvised during the incident.
Infinite retries hide design flaws. If a message bounces between the broker and the consumer for hours, no one is looking at why it fails: it is only avoiding acknowledging that this message will not be processed with the current code. An exponential backoff with a bounded ceiling and a maximum number of attempts leaves the problem in plain sight. From there, the decision is binary: the message goes to a dead-letter queue or is discarded with logging.
Measuring the age of pending messages is more useful than looking at the queue size. A queue with ten thousand messages that drain in seconds is healthy; one with two hundred messages that have not moved for an hour is broken. That indicator, together with the rate of messages that end up in the dead-letter queue, makes it possible to detect accumulation before the system goes down.
Idempotency-key deduplication solves the other extreme: retries that do work but generate duplicates. If the consumer processes the same message twice because the ack was lost, the result depends on the business. For non-idempotent operations, it is advisable to store the processed key with a retention window aligned to the maximum retry time. Without that window, deduplication protects nothing.
The closure includes criteria for deciding when to discard a message and when to alert a human. A message with a validation error is discarded and logged. A message that fails due to a downed dependency is retried. A message that has been in the dead queue too long without anyone reviewing it is an alert, not a pending item.
If you want to see how we apply these policies in real services, you can review the services we document or write to us from the contact page. There are also more technical notes in the blog.