Module A-8·30 min read

Queue depth as the earliest honest signal, admission control, and prioritising traffic when there is not enough capacity for all of it.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module A-8 — Backpressure and Load Shedding

What this module covers: The failure where every dependency is healthy and you are simply over capacity — which behaves nothing like a dependency failure and needs different mechanisms. Why throughput declines past saturation, and why goodput collapses long before throughput does. The central claim: an unbounded queue is not a buffer, it is a latency amplifier that converts errors into timeouts and does 100% of the work for 0% of the value. Backpressure as propagating "slow down" upstream, the mechanisms that actually do it, and the boundary at which backpressure is impossible and shedding is the only option. Which signals to shed on, ranked by how early they tell you the truth — with queue wait time at the top and error rate at the bottom. Adaptive concurrency limits, because any static limit is wrong after the next deploy. Then the part that is a product decision: prioritisation, why retries should be the first traffic you drop, and why LIFO beats FIFO under overload. Finally, why autoscaling is not a substitute, and why recovery needs a ramp.


This Is a Different Failure From the Last Module

Module A-7's mechanisms answer "a dependency I call is broken." This module answers a different question:

Everything downstream is healthy. Every query is fast. And 9,000 requests per second are arriving at a service that can serve 6,000.

No breaker will open, because nothing is failing. No bulkhead helps, because the resource is not being monopolised by one dependency — it is being consumed by legitimate work. What happens instead is that queues grow, latency climbs uniformly, and eventually everything times out at once.

The fact that makes this counterintuitive is that capacity is not a ceiling you bump against; it is a knee after which things get worse:

text

Two terms worth separating, because conflating them hides the whole problem:

  • Throughput: requests completed per second.
  • Goodput: requests completed per second that anybody still wanted — completed inside the caller's deadline.

Past the knee, throughput may look flat or only slightly degraded while goodput falls off a cliff, because the work being completed is work whose caller gave up seconds ago (Module A-6's zombie work). A service can be running at 100% CPU, completing 6,000 requests per second, and delivering a useful 400 — and its throughput graph will look fine.

Then the mechanisms make it worse: clients time out and retry, so offered load rises further (Module A-6's amplification), and you are in the metastable state where the load sustaining the failure is generated by the failure.

Analogy: a restaurant kitchen that takes every order the floor brings. At 30 covers it is fine; at 90 the tickets pile up, and by the time each dish is plated the table has left. The kitchen is working flat out, food is going out at the maximum rate the stoves allow, and nobody is eating. The fix is not a bigger ticket rail — it is the maître d' saying "we are full, forty-five minute wait" at the door. That refusal is the only intervention that makes the guests who are seated get fed.


An Unbounded Queue Is Not a Buffer

"Add a queue so we can absorb the spike" is right for a transient burst and catastrophic for sustained overload, and the arithmetic distinguishing the two is Little's Law again:

queue depth 50,000, throughput 500/s → wait time = 100 seconds

Every one of those 50,000 items will be processed. Every one will be processed after its caller has timed out. So the system does the full amount of work and delivers none of the value — and the memory holding the queue is itself a failure mode.

Worse, the queue changes the failure into a less useful one. Without it, request 6,001 gets an immediate 503: the client knows straight away, can retry with backoff, can fall back, can tell the user. With an unbounded queue, request 6,001 gets accepted and then times out 30 seconds later — the client has waited, held its own resources, learned nothing sooner, and now retries, adding a second item to the queue for work you have already started.

A queue absorbs a burst whose area is smaller than your spare capacity over the burst's duration. It cannot absorb a rate mismatch. If arrivals exceed service rate for a sustained period, a queue only decides how long you spend pretending otherwise.

Hence the rule: every queue is bounded, and the bound comes from the latency target.

max_depth = target_latency × service_rate

Serving at 500/s with a 2-second latency target means a queue of 1,000 and not one item more; beyond that, reject. This applies to your HTTP accept queue, your connection-pool wait queue, your job queue (Module P-19), and every in-process channel. An unbounded queue anywhere in the path is where the latency will accumulate.


Backpressure Is Propagating "Slow Down"; Shedding Is What You Do When You Cannot

Backpressure means the consumer tells the producer to slow down, and the producer can. It is the better mechanism wherever it is available, because no work is discarded — the rate simply matches.

The mechanisms, from the bottom of the stack up:

  • TCP flow control, the original: the receiver advertises a window, and the sender cannot exceed it. Everything below is a re-implementation of this idea.
  • HTTP/2 and gRPC flow-control windows, per-stream and per-connection (Module F-6).
  • Reactive streams / request-n semantics, where the consumer explicitly asks for a number of items.
  • Bounded channels where the producer blocks, as in a Go channel or a semaphore-guarded pipeline.
  • Consumer pause/resume, which is the correct primitive for a Kafka consumer or a queue worker: stop fetching when your internal buffer is full rather than fetching and buffering more (Module P-19's prefetch discussion is this exact issue).
  • Node streams, where writable.write() returning false is backpressure. Ignoring the return value and continuing to write is the standard way to leak memory in a pipeline — the buffer grows without bound because nothing told the producer to stop.
ts

Backpressure works in a closed system — your own pipeline, where the producer is code you control and slowing it down is meaningful. It fails at an open boundary: you cannot tell a million browsers, a partner's integration, or a mobile app fleet to send fewer requests. There is no window to shrink.

At that boundary the only remaining option is load shedding: refuse work, immediately and cheaply, so the work you accept completes. Shedding is not a failure of design; it is the design. The choice is never "shed or serve everything" — it is "shed deliberately, or have the overload choose for you by timing everything out."


What to Shed On: Signals Ranked by How Early They Tell the Truth

SignalQualityWhy
Queue wait timebestit is the latency you are about to violate; leads everything else
Concurrency in flight vs limitvery gooddirect, cheap, and comparable to a computed capacity
Queue depthgoodneeds a service rate to interpret, but cheap and immediate
Event-loop delay (Node) / run-queue lengthgoodmeasures the actual contended resource
CPU utilisationmediocresaturation is not linear in CPU, and I/O-bound work has low CPU while queueing
Latency p99laggingby the time p99 moves, the queue is already deep
Error rateworstit is the outcome you were trying to prevent

The principle: prefer ingress signals that lead over egress signals that lag. By the time your error rate rises you are minutes into the incident; by the time queue wait time exceeds your target you are milliseconds into it.

The best-known implementation of the top row is CoDel (controlled delay), borrowed from network queue management: track the minimum queue delay over a sliding window, and when the minimum stays above a target (say 5 ms) for longer than an interval (say 100 ms), start dropping. Using the minimum rather than the average is the clever part — it distinguishes a standing queue, which is pure latency, from a brief burst passing through, which is fine.

ts

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.