← Back to Blog

How I Fixed Message Queue Backlogs with DLQs

· 5 min read
message-queues distributed-systems queue-management performance

I’ll never forget the morning I got a frantic Slack message from my team lead: "Payment confirmations are piling up in the queue!" It was 2 AM, and my payment processing service had just hit a 12-hour backlog. I’d built this system using RabbitMQ since early 2024, but something had gone wrong with the flow. We’d been getting 500+ payment messages per minute—no problem until the queue depth hit 10,000 messages and kept growing.

At first, I thought it was just a temporary spike, but the issue persisted for three days. I dug into the logs and noticed something disturbing: messages were stuck in the queue for 20 minutes or more. The service was processing messages at 500/sec, yet the queue depth kept rising. We were clearly missing something obvious.

Why Message Queues Are Not Always "Set and Forget"

If you’ve worked with message queues for more than a year, you know RabbitMQ (or other brokers) are powerful. But they’re not magic—it’s easy to assume you’re done once your producer and consumer are up. In my case, I’d ignored the most basic principle: you need to handle dead letters.

When a message is rejected or fails to process, it gets moved to a dead-letter queue (DLQ) by default. But if you don’t configure the DLQ or monitor it, messages pile up there invisibly—often causing downstream failures. In my case, the DLQ wasn’t set up properly, and messages kept getting stuck in the "retries" bucket because the dead-letter configuration was missing.

This wasn’t an "edge case"—it was a flaw in my architecture. The system wasn’t designed for high-volume traffic, and we never tested with realistic failure scenarios. I’d been running it at 500 messages/sec for months without issue, but then a 15-minute payment surge hit, and it broke.

How I Fixed It: Dead-Letter Queues & Monitoring

The fix? Two simple changes:

  1. Set up a dedicated DLQ configuration. I created a separate queue for failed messages with a max-retries limit. When messages failed to process, they’d get routed to this DLQ instead of getting stuck forever in the main queue.

  2. Add monitoring to the DLQ. I used Prometheus to track the DLQ queue depth and set up alerts when it exceeded 100 messages. This helped us catch issues before they cascaded.

Here’s the RabbitMQ configuration I used—this is how I set up the DLQ for my payment service:

{
  rabbitmq_config: [
    {queue, "payment-queue", [
      {args, [
        {x-queue-mode, "lazy"},
        {x-overflow, "reject"},
        {x-queue-type, "quorum"}
      ]},
      {dead_letter_exchange, "dead-letter-exchange"},
      {dead_letter_routing_key, "payment-failures"},
      {max_retries, 5}
    ]},
    {exchange, "dead-letter-exchange", [
      {type, "direct"}
    ]},
    {queue, "payment-failures", [
      {args, [
        {x-queue-type, "quorum"}
      ]}
    ]}
  ]
}

I added this to my RabbitMQ configuration and set up a retry policy in the consumer:

def process_payment(message):
    try:
        # handle payment logic
    except Exception as e:
        if current_retry_count >= max_retries:
            # route to DLQ
            publish_to_dlq(message)
            return
        else:
            # retry after 1 second
            sleep(1)
            process_payment(message)

Within a day, the queue depth stabilized. Messages stopped piling up, and the service resumed normal operations. The best part? I’d been using the DLQ for weeks without any issues. The problem had been hidden in plain sight.

Why This Works (and Why I Almost Missed It)

I learned three critical lessons from this incident:

First, DLQs aren’t optional. If you’re using a message queue, you must configure the dead-letter policy. Without it, messages stuck in the queue will silently cause failures.

Second, monitor everything. I had alerts on queue depth, but not on DLQ. This is the trap—messages getting stuck in the DLQ often don’t trigger the main queue depth alarm until they’ve piled up for hours.

Third, test with realistic failure scenarios. My team had never run a 15-minute payment surge test. The system was built for 100% uptime, but not for what happens when the traffic spikes and failures occur.

What I Changed (and What I Keep)

After this incident, I rewrote our message processing pipeline. I added these changes to our configuration:

  • Default queue depth limit of 50,000 messages
  • DLQ auto-backoff (messages automatically retry after 1 minute)
  • Prometheus monitoring for both the main and DLQ queues
  • A custom error tracker that logs every message failure

I also started testing for failure scenarios at least twice a month. We now simulate traffic spikes with the same load-generating tools we used for the backlog.

And the best part? I no longer get that 2 AM Slack message. The system is robust and handles failures gracefully.

My Final Thoughts

Message queues are amazing, but they’re not magic. If you’re working on a high-throughput system, DLQs are your unsung hero. They’re not a luxury—they’re a necessity. If you’ve been using RabbitMQ or other brokers without proper DLQ configuration, I encourage you to review your setup.

The truth is, message queues will break if you don’t handle dead letters. And the only way to avoid them? You have to monitor your DLQs and configure them correctly from the start.

If you’ve ever dealt with a queue backlog, I’m guessing you’ve wondered why you didn’t notice the issue sooner. The reality? You often don’t. But if you watch your DLQ depth, you’ll catch it before it snowballs into a full-blown outage.

Now, go check your message queues. It’s the only way to ensure you’re not missing messages in the dark. And trust me—this will save you a ton of headaches. Ever wondered why your messages keep getting stuck? Maybe it’s not your code. Maybe it’s your DLQ. Check it out. You’ll thank yourself later.