b8c.io All articles
Product & Delivery

Job Queue Debt: How Async Processing Goes From Clever Shortcut to Cost Center

b8c.io
Job Queue Debt: How Async Processing Goes From Clever Shortcut to Cost Center

Photo: server infrastructure background job processing queue technology concept, via www.racksolutions.com

Somewhere in your infrastructure, there's probably a queue that nobody fully owns anymore. It was added two years ago by an engineer who has since moved to another team. It processes... something. Maybe email notifications. Maybe thumbnail generation. Maybe a batch job that was supposed to be temporary but became load-bearing.

And it's quietly costing you money every single month.

Async processing is one of those architectural patterns that feels universally good when you're implementing it. Offload the heavy lifting, unblock the API response, scale the workers independently — what's not to like? The problem shows up later, gradually, in the form of infrastructure bills that don't quite make sense and incidents that are nearly impossible to debug.

Why Every Team Reaches for the Queue

The appeal is completely understandable. You've got a user-facing endpoint that's getting slow because it's doing too much: sending a confirmation email, updating analytics, generating a PDF, kicking off a third-party webhook. The synchronous version of that endpoint takes 4.2 seconds. That's a conversion killer.

So you reach for a queue. Sidekiq, Celery, BullMQ, SQS — doesn't matter. You push the work off the critical path, the endpoint drops to 200ms, and everyone's happy. The PR gets merged, the feature ships, the metric improves.

What the PR doesn't capture is what happens when that job fails. Or when it succeeds but produces a wrong result. Or when the downstream service it depends on goes down for six minutes and your queue depth spikes to 400,000 jobs. Or when the retry logic you wrote at 11 p.m. turns a single failed dependency into a thundering herd that takes down a database.

Those scenarios live in the future, and future problems are easy to discount.

The Three Ways Queues Become Sinkholes

Dead-letter accumulation. Every serious queue implementation has a dead-letter queue — the place jobs go when they've exhausted their retries. In theory, someone monitors this queue and triages failures. In practice, dead-letter queues are often the digital equivalent of the junk drawer in your kitchen. They grow. Nobody looks at them. Then one day you realize you've got 2.3 million unprocessed jobs representing real user actions that silently failed months ago.

One e-commerce company I'm aware of discovered their dead-letter queue contained unprocessed order confirmation emails going back fourteen months. Customers had stopped complaining (they assumed the emails were spam-filtered), but the brand trust damage was already done — and the compute cost of storing and occasionally retrying those jobs had added up to something embarrassing.

Retry storms. Retry logic is necessary. Exponential backoff is table stakes. But retry storms happen when your backoff isn't actually exponential in practice, when your jitter isn't sufficient, or when you've got multiple queue consumers all retrying at roughly the same cadence against a recovering dependency.

The math here gets ugly fast. If you have 50,000 jobs that failed because a downstream API was unavailable for ten minutes, and they all retry with a one-minute backoff and no jitter, you've just scheduled 50,000 simultaneous API calls sixty seconds from now. That's not recovery — that's a second incident stacked on top of the first.

Worker sprawl. This one is sneaky. Workers are cheap to spin up and easy to forget. A team adds a dedicated worker pool for a high-priority feature launch. The launch succeeds. The feature matures. The load drops. But the worker pool stays at its launch-day configuration because nobody ever went back to right-size it.

Multiply this by the number of features your team has shipped over the past three years and you've got a worker fleet that's running at 8% average utilization but billing at 100%. Cloud providers don't give discounts for idle compute.

The Hidden Complexity Tax

Beyond the direct cost, async processing introduces a category of complexity that's easy to underestimate: observability and correctness guarantees.

Synchronous code is relatively easy to reason about. The function runs, it succeeds or fails, you know immediately. Async code introduces temporal decoupling — the job runs later, in a different process, possibly on a different server, definitely without the request context that triggered it.

This creates real debugging challenges. When a user reports that their account wasn't updated after a certain action, your first question is: did the job run? Did it succeed? Did it run but produce an incorrect result? Did it run multiple times? Without meticulous job logging and idempotency guarantees, answering these questions requires a forensic investigation that can eat hours.

Idempotency is worth dwelling on. Jobs will run more than once. Networks are unreliable, consumers crash, at-least-once delivery is the default in most queue systems. If your job isn't idempotent — if running it twice produces a different result than running it once — you've got a correctness bug waiting for the right failure scenario to surface it.

When Async Is Actually the Right Call

None of this means you should avoid async processing. It means you should use it deliberately.

Async is genuinely the right choice when:

Async is the wrong choice when:

Getting Your Queues Back Under Control

Start with an inventory. List every queue in your system, who owns it, what it processes, and what the failure behavior is. You'll find queues that nobody can fully explain. That's your technical debt.

For each queue, define a runbook: what does a healthy queue look like? What's the acceptable depth, processing latency, and failure rate? Without a baseline, you can't alert on anomalies.

Audit your retry configuration. Make sure exponential backoff is actually exponential, jitter is applied, and maximum retry counts are set. An infinite retry loop is just a slow DDoS attack on your own infrastructure.

Right-size your workers on a quarterly cadence. Utilization data is usually available — use it. The workers you spun up for Black Friday probably don't need to run at Black Friday capacity in March.

Async processing is a powerful tool. But like any tool, it has a cost of ownership that doesn't show up in the initial implementation PR. The teams that use it well are the ones who treat job queues as production systems — with the same monitoring, ownership, and operational discipline they'd apply to any other part of the stack.

All Articles

Related Articles

Stop Architecting for Unicorn Scale Before You Have Unicorn Users

Stop Architecting for Unicorn Scale Before You Have Unicorn Users

The Open Source Trap: Why 'Free' Dependencies Can Cost You More Than a SaaS Subscription

Why 90% of Developers Ghost Your API After the First Call