DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Why Queues Don’t Fix Scaling Problems
  • What We Learned Migrating to a Pub/Sub Architecture: Real-World Case Studies from High-Traffic Systems
  • Event-Driven Microservices: How Kafka and RabbitMQ Power Scalable Systems
  • Building an Event-Driven Architecture Using Kafka

Trending

  • Using LLMs to Automate Data Cleaning and Transformation Pipelines
  • Run Gemma 4 on Your Laptop: A Hands-On Guide to Google's Latest Open Multimodal LLM
  • Offline-First Patch Management for 10,000 Edge Nodes: A Practical Architecture That Scales
  • A Hands-On ABAP RESTful Programming Model Guide
  1. DZone
  2. Data Engineering
  3. Big Data
  4. Queues Don't Absorb Load — They Delay Bankruptcy

Queues Don't Absorb Load — They Delay Bankruptcy

Queues hide overload. Without back-pressure, limits, and scaling, lag just grows until failure. Bound queues, alert on lag, fail fast, and plan capacity.

By 
David Iyanu Jonathan user avatar
David Iyanu Jonathan
·
Mar. 30, 26 · Opinion
Likes (2)
Comment
Save
Tweet
Share
1.6K Views

Join the DZone community and get the full member experience.

Join For Free

There's a version of this story that every backend engineer has lived through at least once. Traffic spikes, latency climbs, someone suggests a queue, the queue gets added, and for a while — maybe twenty minutes, maybe two hours — everything looks stable again. Dashboards green. On-call engineer breathes. And then, with the particular cruelty of systems that have been quietly filling up while you weren't watching, the whole thing falls apart worse than before.

The queue didn't save you. It gave you a longer runway before the same cliff.

What a queue actually does, mechanically, is decouple production rate from consumption rate. That's the pitch, and it's real. A Kafka topic absorbs bursty write traffic and lets consumers process at their own pace. Fine. Useful. But the deception is in the word absorb — it implies the work disappears, gets neutralized, becomes manageable. It doesn't. The work is still there. Every event you enqueued is still waiting to be processed, and if your consumers can't keep up with steady-state input, the lag will grow monotonically. Not spike and recover. Grow. Forever, until something breaks.

Think of it like debt, not a buffer. You're borrowing time against a credit limit that is, in most default configurations, embarrassingly generous or entirely absent. An unbounded in-memory queue will take everything you hand it until the heap exhausts. RabbitMQ with no x-max-length will happily accept messages until the broker runs out of memory and crashes, at which point you've lost the messages and the broker. Kafka is slightly more honest about this — partition retention limits will eventually cause data loss too, it just takes longer and the surprise feels worse when it arrives because you trusted the log.

The failure mode people miss isn't the catastrophic OOM crash. It's the slow degradation. Queue depth grows from 1,000 to 10,000 to 100,000 over the course of an afternoon. Consumers are working hard, they just can't outrun the firehose. Latency through the system — from event production to event processing — climbs from 200ms to 45 minutes. Your users aren't seeing errors. They're seeing stale data, delayed notifications, orders that confirmed but didn't process. Technically the system is "up." Functionally it has already failed and no alert fired.

The reason teams reach for queues reflexively during load events is that the queue makes the producer feel better immediately. API response times drop. The work gets handed off. The caller gets a 202 and moves on. What you've done is move the pain downstream, out of the request path and into the consumer tier, where it's less visible and where the product manager can't directly observe it.

This is not purely cynical — there are absolutely cases where async processing is the right architecture and the queue is doing real work. Email delivery, image resizing, report generation: tasks where the user has implicitly consented to waiting. But those systems still require explicit capacity planning for the consumer side, and they still require bounded queues with overflow handling. The mistake is treating the queue as the end of the design conversation rather than the beginning of a harder one.

What does careful look like? It starts with back-pressure — not as a conceptual gesture but as a concrete mechanism that runs end-to-end.

Back-pressure means that a slow consumer has a way to signal the producer to slow down. In gRPC, this is built into the transport via flow control: if the receiving end isn't reading fast enough, TCP's receive buffer fills, the sender blocks, and you get natural throttling without any application-layer logic. In Kafka, there's no native back-pressure from consumer to producer — you have to build it yourself, which is why it's rarely there when you need it. One approach is producer-side rate limiting keyed off consumer lag metrics: if the Kafka consumer group lag for a given topic exceeds a threshold, an upstream service reads that metric and starts applying a token bucket or leaky bucket to its own write rate. Ugly? A bit. Effective? Yes.

Akka Streams and Project Reactor both have back-pressure baked into their execution models — demand is explicitly signaled upstream, and a slow subscriber will eventually slow the source. If you're running JVM workloads and you haven't looked at these, the operational behavior under overload is genuinely different from ad-hoc queue implementations. Not a silver bullet — the back-pressure can cause its own problems if upstream blocking is expensive — but at least failure is deterministic.

The simpler thing, which doesn't require a streaming framework, is bounded queues with explicit rejection. Set maxQueueSize in your Tomcat connector config. Set x-max-length on your RabbitMQ queues. Configure queue.max.bytes somewhere in your pipeline. When the queue is full, reject the work. Return HTTP 429. Log it. Alert on it. This is uncomfortable because it means admitting to callers that you're overloaded, but it's far less uncomfortable than silent data loss six hours later.

The dead-letter queue is worth mentioning because it's often deployed as a solution when it's actually a diagnostic tool. Messages that can't be processed or that overflow a bounded queue get routed to the DLQ, which is fine — you can inspect them, replay them, understand what failed. But if your DLQ is filling up because your primary queue can't keep pace, the DLQ is not protecting you, it's just giving the unprocessed work a second place to sit. You still have to deal with it. The capacity problem is the same.

Priority queues are underused and genuinely help in heterogeneous workloads. If you have both critical payment events and low-stakes analytics events flowing through the same consumer pool, and the consumers get overwhelmed, the analytics events should die first. Not the payments. Most teams don't implement this until they've had the incident where the reverse happened.

The circuit breaker pattern, applied to queues, is worth thinking through carefully. The classic Hystrix-style breaker trips when downstream error rates exceed a threshold, and subsequent calls fail immediately rather than queuing up. In the queue context, this means: when consumer lag exceeds a threshold, stop accepting work into the queue and fail fast at the API layer. The queue never grows because new requests get 503'd before they enter the system. This preserves the health of whatever is already queued and gives consumers a chance to drain. When lag drops below the threshold, the breaker closes and normal operation resumes.

Whether this is the right tradeoff depends entirely on your product. Failing fast is good for callers who can retry with backoff. It's terrible for callers who don't expect errors and don't retry. Know your consumers before you implement this.

On the infrastructure side, autoscaling consumers feels like it should solve everything and mostly doesn't. The problem is the scaling latency. If you're on EC2 with an ASG, spinning up a new consumer might take 3-5 minutes. If your queue is filling in 60 seconds under load, autoscaling won't catch it. Fargate is faster but still not instant. Serverless consumers (Lambda on SQS) get you closer to elastic capacity, but Lambda has its own concurrency limits and its own failure modes under heavy load — particularly around batching behavior and function throttling. You can absolutely build a system where SQS triggers Lambda at scale, but you need to think through what happens when Lambda gets throttled and messages start timing out on their visibility window and get requeued and processed twice.

The thing that actually works is a combination: autoscaling for sustained load, bounded queues for burst, and explicit rate limiting at the ingestion point so the burst itself is smoothed rather than absorbed whole.

If I were going back to a codebase on Monday and I had concerns about queue behavior under load, I'd do this:

First, find every unbounded queue in the system and make it bounded. This alone catches the worst failure modes. If rejecting overflow messages would cause data loss you can't accept, you need a persistence story for those overflow cases — not a bigger queue.

Second, instrument queue depth and consumer lag as first-class metrics with paging alerts, not just dashboards. A queue that's at 80% capacity during normal operations is a ticking problem, and nobody will notice without an alert.

Third, run a sustained overload test — not a spike, a sustained overload — and watch what happens over 20 minutes. Does the queue drain? Does it grow? Does it plateau? Where does memory go? This is the test most teams skip because it's uncomfortable to deliberately degrade a staging environment, and it's the one test that would have caught most of the incidents I've described.

Fourth, make sure the callers of your async endpoints handle 429s and 503s with backoff. An overloaded service returning 429 helps nobody if clients immediately retry at full rate. The throttle has to propagate.

There's a version of this discipline that gets sold as resilience engineering, and there's consulting money in it, and it's legitimate work. Aperture, the Fluxninja tool, does weighted fair queuing in front of services and has real operational value in mixed-criticality environments. API management platforms that auto-throttle based on downstream health signals exist and are useful. But most of the improvements I've seen come from much less glamorous changes: adding a size limit to a queue, writing an alert on consumer lag, doing a single overload drill in staging. The fundamentals aren't glamorous but they're where the real failures live.

The uncomfortable truth about queues is that they feel like they're solving a capacity problem when they're actually making a capacity problem invisible for a while. A system at its actual capacity needs to either refuse work or add capacity. There is no third option that doesn't involve the work piling up somewhere until something gives. A queue is the place it piles up. That's all it is.

IT Event kafka systems

Opinions expressed by DZone contributors are their own.

Related

  • Why Queues Don’t Fix Scaling Problems
  • What We Learned Migrating to a Pub/Sub Architecture: Real-World Case Studies from High-Traffic Systems
  • Event-Driven Microservices: How Kafka and RabbitMQ Power Scalable Systems
  • Building an Event-Driven Architecture Using Kafka

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook