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:
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 falseis 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
Signal
Quality
Why
Queue wait time
best
it is the latency you are about to violate; leads everything else
Concurrency in flight vs limit
very good
direct, cheap, and comparable to a computed capacity
Queue depth
good
needs a service rate to interpret, but cheap and immediate
Event-loop delay (Node) / run-queue length
good
measures the actual contended resource
CPU utilisation
mediocre
saturation is not linear in CPU, and I/O-bound work has low CPU while queueing
Latency p99
lagging
by the time p99 moves, the queue is already deep
Error rate
worst
it 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
For Node specifically, event-loop delay is an excellent local signal because it measures the resource actually being contended:
ts
Adaptive concurrency limits, because static limits are always wrong
A fixed concurrency limit ("at most 200 in flight") is a guess that was correct at one moment. It becomes wrong when the code gets slower, the data grows, the instance type changes, or a dependency's latency shifts — and being wrong in either direction is costly: too low wastes capacity you paid for, too high fails to protect.
Better is to estimate capacity continuously, using the same idea as TCP congestion control:
Little's Law gives capacity ≈ throughput × minimum_observed_latency.
If current latency is close to the minimum, the system is not queueing — increase the limit.
If latency rises above the minimum by some gradient, a queue is forming — decrease the limit.
AIMD (additive increase, multiplicative decrease) or a gradient algorithm keeps it tracking.
This is what the Netflix concurrency-limits family of algorithms (Vegas, Gradient2) does, and the reason it is worth knowing about is that it removes a tuning parameter that nobody re-tunes. A static limit set during a load test in March is wrong by June; an adaptive limit is not.
Reject before doing work
A shed response must be genuinely cheap, which means rejecting at the earliest point in the stack — before authentication, before deserialising a large body, before touching the database. This is Module P-18's point restated: a 503 that costs as much as a 200 protects nothing.
The best rejection points, cheapest first: the load balancer or gateway (which never reaches you), the first middleware, and — the one most often overlooked — the connection-pool acquire timeout:
ts
An unbounded (or 30-second) pool wait is an unbounded queue in the place it does most damage (Module P-4), and turning it into a 50 ms admission decision is one of the highest-value changes available.
Prioritisation Is the Actual Deliverable
Shedding uniformly is better than not shedding, and much worse than shedding well. Not all requests are equally valuable, so classify them — and this is a product decision that should be written down, in the same way Module A-7's degradation table is.
Retries should be the first thing you shed. They are the amplification from Module A-6, arriving exactly when you can least afford them. Mark them at the source — a header such as x-retry-attempt: 2 — so the receiving side can drop them preferentially. This is a small change with a large effect: it means an overloaded service naturally reduces the amplification pressure instead of treating a retry as equal to a first attempt. It is also the mechanism that lets you shed without triggering more retries, which is the loop that keeps overload alive.
Analytics and telemetry beacons are sheddable, and dropping them must not fail a user request. A service that fails a checkout because a metrics write timed out has inverted its priorities in a way that is embarrassingly common.
LIFO under overload, which is counterintuitive and correct
The default queue discipline is FIFO, which is fair and, under overload, wrong. Consider a queue where the oldest item has been waiting 40 seconds and the client's timeout is 10 seconds:
text
Under overload, the oldest work is the least valuable because it is closest to (or past) its deadline. So serve newest-first, or better, make the queue deadline-aware: attach each request's deadline (Module A-6) and drop anything already expired without processing it at all. That single check — "is this request already dead?" before starting work — recovers a large fraction of capacity during an incident, at essentially no cost.
FIFO remains correct under normal load, where fairness matters and nothing is near its deadline. The switch is part of the overload response, not the default.
Fairness across tenants
Shedding uniformly means a single tenant's traffic surge degrades everybody proportionally. That is usually the wrong distribution: the fair response is to shed the excess of the heaviest consumers first, preserving the service of tenants operating within their normal envelope. This is Module P-18's per-tenant limiting doing double duty as a shedding policy, and Module P-8's noisy-neighbour containment expressed as an admission decision.
Respond Honestly, and Do Not Confuse Shedding With Degradation
The shed response is 503 Service Unavailable with a Retry-After — not 500 (which implies a bug), not 429 (which implies the client exceeded a limit they agreed to, rather than you being over capacity), and never a 200 with empty data.
That last one matters. Returning 200 with an empty list when you meant "I could not do this" is degradation dressed as success, and it has the failure shape Module A-7 warned about: nothing is recorded as an error, so the incident is invisible, and clients cache and propagate an empty answer as though it were true. If you genuinely have a good fallback, that is degradation and it should be reported as a degraded-serve, not as a normal success.
And the client side of the contract: honour Retry-After, count the shed against your retry budget, and do not translate a 503 into three immediate retries (Module A-6).
Autoscaling Is Not a Substitute, and Recovery Needs a Ramp
The obvious objection is "why shed when we can scale?" Because the timescales differ by three orders of magnitude:
Time to take effect
Load shedding
microseconds
Adaptive concurrency limit adjusting
seconds
Autoscaling a container
30 s – 2 min (image pull, start, warm-up, readiness)
Autoscaling a VM
2 – 5 min
Adding a database replica
minutes to hours
Scaling handles trends; shedding handles spikes. A flash sale arriving in 20 seconds will be over, or will have taken you down, before an autoscaler finishes reacting.
Worse, scaling can make an overload worse in three specific ways:
More instances means more database connections — instances × pool (Modules P-4, A-4) — so scaling the stateless tier can saturate the stateful one that was the actual bottleneck.
New instances have cold caches, so their per-request cost is higher and they generate more downstream load per request than the warm ones they joined.
If the bottleneck is not the thing you scaled, you have added cost and load without adding capacity.
So the correct posture is both: shed to survive the next 60 seconds, scale to handle the next 10 minutes, and make sure the scaling target is the actual bottleneck.
Finally, recovery needs a ramp. When the overload passes and you stop shedding, all the traffic you were rejecting — plus the retries it generated — arrives at once against a fleet with cold caches. Reopen gradually: increase the admission limit progressively (slow start, exactly as TCP does), and let the adaptive limiter find the new ceiling rather than jumping straight back to the old one. A recovery that re-saturates is how a five-minute incident becomes a series of five-minute incidents.
Why this matters in production: overload is the failure mode where doing nothing is worst and doing something crude is nearly as good as doing something sophisticated. A service with a bounded queue, a short pool-acquire timeout, and a 503 above a concurrency limit survives a 3× spike with degraded but positive goodput. The same service with unbounded queues serves nothing for twenty minutes, keeps its CPU at 100% the whole time, and recovers only when a human removes it from rotation to let the queues drain. The gap between those two outcomes is a day of work, and the more sophisticated additions — CoDel, adaptive limits, priority classes — are refinements on top of a bound that either exists or does not.
Where This Shows Up in Your Stack
Node.js:monitorEventLoopDelay is the best local overload signal available, because the event loop is the resource actually being contended. Respect stream backpressure (write() returning false, or just use pipeline()), or you will leak memory under load. Put a bounded queue with rejection in front of expensive handlers, and remember a synchronous CPU-bound section blocks your own shedding logic (Module P-19).
PostgreSQL: the connection pool's wait queue is the queue that matters most, and connectionTimeoutMillis is admission control — set it to tens of milliseconds, not 30 seconds, so a request that cannot get a connection fails immediately instead of joining an unbounded queue (Module P-4). max_connections is the hard bound, statement_timeout prevents one query from occupying a slot indefinitely (Module A-6), and separate pools per workload keep the shedding decisions independent (Module A-7).
Kafka: consumer lag is a backpressure signal, and pause()/resume() is the correct primitive — stop fetching when your buffer is full rather than fetching more and buffering (Module P-15). On the producer side, buffer.memory filling causes send() to block, which is backpressure reaching your application; treating that block as an error to be retried converts backpressure into amplification.
Redis:client-output-buffer-limit disconnects a client that cannot keep up with what it subscribed to, which is Redis shedding a slow consumer rather than growing memory without bound — the same principle applied by the server to you.
Load balancers and gateways: the cheapest rejection point, since a request refused there never touches your process. Configure surge queues and spillover deliberately: a large LB queue is an unbounded queue you did not know you had, in the place with the least visibility.
Service mesh: connection-pool limits and pending-request limits at the proxy give you per-dependency admission control without application code (Module A-5), which pairs with the application-level priority classes the proxy cannot know about.
Summary
Overload is a different failure from dependency failure. Everything downstream is healthy; you are simply over capacity, so no breaker opens and no bulkhead helps.
Capacity is a knee, not a ceiling. Past it, goodput — work completed inside the caller's deadline — collapses while throughput can look flat, because the work being completed is work whose caller left. A service can run at 100% CPU completing 6,000 requests per second and deliver a useful 400.
An unbounded queue is not a buffer. A depth of 50,000 at 500/s is 100 seconds of wait: every item is processed, every item is processed too late, so you do 100% of the work for 0% of the value — and you have converted an immediate 503 the client could act on into a timeout it learns about 30 seconds later and then retries.
A queue absorbs a burst smaller than your spare capacity over its duration; it cannot absorb a rate mismatch. So bound every queue, with max_depth = target_latency × service_rate, and reject beyond it — including the HTTP accept queue, the pool wait queue, job queues and in-process channels.
Backpressure propagates "slow down" upstream and discards nothing — TCP windows, HTTP/2 flow control, request-n streams, bounded blocking channels, consumer pause(), and Node's write() returning false, which is the signal people ignore and thereby leak memory. It works in a closed pipeline you control and is impossible at an open boundary, where you cannot ask a million browsers to send less.
At an open boundary, shedding is the design, not a failure of it. The choice is to shed deliberately or let the overload shed for you by timing everything out.
Rank your signals by lead time: queue wait time is best (it is the latency you are about to violate), then in-flight concurrency, queue depth, and event-loop delay; CPU is mediocre; p99 latency lags; error rate is the outcome you were preventing. CoDel uses the minimum queue delay over a window precisely to distinguish a standing queue from a burst passing through.
Static concurrency limits are always wrong eventually — after a deploy, a data-size change, or an instance-type change — so estimate capacity continuously from throughput and minimum observed latency and adjust with AIMD or a gradient algorithm. The value is removing a tuning parameter nobody re-tunes.
Reject before doing work. The cheapest points are the load balancer, the first middleware, and the connection-pool acquire timeout, which turns an unbounded pool wait into a 50 ms admission decision — one of the highest-value single changes available.
Prioritisation is the actual deliverable, and it is a product decision. Critical-plus (payments, auth) last; sheddable (prefetch, analytics, retries) first. Mark retries with a header so they can be shed preferentially — it reduces amplification exactly when you cannot afford it, and it lets you shed without triggering more retries.
Under overload, LIFO beats FIFO, because the oldest work is closest to or past its deadline: serving it spends capacity on doomed requests and dooms everything behind it. Better still, make the queue deadline-aware and drop already-expired requests without processing them at all — a nearly free check that recovers a large share of capacity during an incident. FIFO remains right under normal load.
Shed the excess of the heaviest tenants first, not uniformly, or one tenant's surge degrades everyone proportionally (Modules P-8, P-18).
Respond with 503 and Retry-After. Never a 200 with empty data — that is degradation dressed as success, invisible to your error rate and cached by clients as though true.
Autoscaling is not a substitute: shedding acts in microseconds, autoscaling in minutes. Scaling handles trends, shedding handles spikes. And scaling can worsen an overload by multiplying database connections, adding cold-cache instances with higher per-request cost, or expanding a tier that was never the bottleneck.
Recovery needs a ramp. When you stop shedding, the rejected traffic plus its retries arrives at once against cold caches, so reopen progressively and let the adaptive limiter rediscover the ceiling — otherwise one incident becomes a series of them.
Crude beats absent. A bounded queue, a short pool-acquire timeout and a 503 above a concurrency limit survive a 3× spike with degraded but positive goodput; the same service with unbounded queues serves nothing for twenty minutes and recovers only when a human takes it out of rotation.
The next module, Multi-Region Architecture, changes the axis from capacity to geography. Everything so far has assumed one place; A-9 asks what changes when there are several — active-passive versus active-active, how requests get routed to the right region, what data residency law does to your topology, and the ordering problem from Module A-1 in its most expensive form, where two regions accept conflicting writes and last-write-wins silently discards one of them.
Knowledge Check
During a traffic spike, a service's dashboards show CPU at 100%, throughput steady at its usual 6,000 requests per second, and error rate near zero — yet users report the product is unusable and support is flooded. The service has a large in-memory work queue that has grown to 90,000 items. What is happening?
A team adds load shedding above a concurrency limit, returning 503. Under overload, offered load climbs *higher* than before shedding was introduced, and the incident lasts longer. Clients are internal services with standard retry logic. What does the module identify as the missing pieces?
A job-processing service uses a FIFO queue. During a sustained overload where client deadlines are 10 seconds and queue wait time has reached 40 seconds, an engineer proposes switching the overload behaviour to serve newest-first and to discard any queued item whose deadline has already passed. A colleague objects that this is unfair. How does the module resolve it?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
// The anti-pattern: writes as fast as the source produces, buffers without bound.source.on("data", chunk => dest.write(chunk));// Correct: honour the signal. Or just use pipeline(), which does it for you.awaitpipeline(source, transform, dest);
// Sketch: reject when the queue has been persistently slow, not on a single spike.if(queue.minDelayOver(100/*ms*/)>5/*ms target*/)returnshed(req);
import{ monitorEventLoopDelay }from"node:perf_hooks";const h =monitorEventLoopDelay({ resolution:10});h.enable();// Shed when the loop is persistently behind: the process cannot keep up, full stop.constoverloaded=()=> h.mean /1e6>70;// ms
// A short acquire timeout IS admission control: if no connection is free within// 50ms, the request cannot be served in time anyway, so fail it now.const pool =newPool({ max:20, connectionTimeoutMillis:50});
FIFO: serve the 40s-old item first — its caller left 30 seconds ago.
You spend capacity on doomed work, so the NEXT item also waits 40s,
and every item in the queue is doomed. Goodput → 0.
LIFO: serve the newest item — its caller is still waiting.
Some requests succeed. The old items are dropped, which is the honest
outcome for work nobody wants. Goodput stays positive.