Load Shedding and Graceful Degradation: Failing the Right Requests First
Here is the counterintuitive fact at the heart of overload engineering: a server pushed past its capacity does not do less useful work proportionally — it often does almost none. Queues grow, every request waits longer, clients time out and retry (adding load), and the server ends up burning all its CPU processing requests whose callers already gave up. Throughput stays high; goodput — responses that arrive in time to matter — collapses toward zero. This is congestion collapse, and it is the failure mode behind a large fraction of famous outages.
The escape is admitting a truth most teams resist: past saturation, some requests must fail. The engineering question is never "how do we serve everything" — it is "who do we fail, how fast, and how cheaply." Done well, an overloaded system sheds the least valuable work early and keeps its core promise intact. Done badly, it fails everyone equally, slowly.
Queues are where latency goes to hide
The mechanics come straight from Little's law: concurrency = arrival rate × service time. When arrivals exceed what your concurrency can service, the excess must queue, and queue wait grows without bound. An unbounded queue converts overload into unbounded latency — the worst possible response, because a request served after its caller's timeout is pure waste that still cost full price to compute.
So the first interventions are structural:
1. Bound every queue. A full queue rejects instantly — cheap failure.
2. Cap concurrency explicitly. Thread pools, connection pools, semaphores.
3. Reject > queue. A fast 429/503 costs microseconds; a timed-out
request costs the full service time and helps no one.
4. Deadline-check before work. If the request's deadline already passed while
queued, drop it — the client is gone.
Deadline propagation multiplies the value of point 4: the edge assigns a budget (say 800ms) and every hop passes the remainder downstream. A backend that receives 20ms of remaining budget for a 50ms operation should fail immediately — spending the 50ms produces an answer nobody is waiting for. gRPC carries deadlines natively; using them is free correctness. This is backpressure doing its job: pushing the "no" as close to the source as possible, where it is cheapest.
Not all requests are worth the same
Uniform rejection is fair and wrong. Checkout and "recently viewed items" are not equally important, and overload is precisely when the difference matters. Mature systems attach a criticality tier to every request — CRITICAL, DEFAULT, BEST_EFFORT is enough — propagated like deadlines, and shed from the bottom:
load 70%+ → shed BEST_EFFORT (prefetch, analytics, recommendations)
load 85%+ → shed DEFAULT (non-purchase browse paths degrade)
load 95%+ → CRITICAL only (checkout, payment, auth)
Google's systems work this way internally; retail architectures do the equivalent when search gets slow but checkout never does. Two implementation notes that separate theory from production. First, the load signal must be local and fast — queue depth, concurrency in flight, p99 service time — not a metrics-pipeline average that arrives 30 seconds late. Second, shed early in the request path, at admission, before the expensive downstream fan-out; rejecting after you have done the work is theater. This composes with rate limiting: quotas cap what each client may send in the steady state, shedding decides what the server will do when the aggregate exceeds reality anyway.
Degradation is shedding's gentler sibling: instead of failing the request, serve a cheaper version. Stale cache instead of a live query. Popular-items instead of personalized ranking. Search over the hot index instead of the full corpus. Each of these is a pre-built fallback path — the crucial word being pre-built: a degraded mode invented during the incident is just a second incident. Netflix's fallback-heavy architecture and every "serving stale while origin recovers" CDN behavior are this pattern institutionalized.
Retries: the load amplifier hiding in your client config
Now the self-inflicted half. A dependency slows down; callers time out and retry; three configured attempts turn 1x load into 3x against a service that was already drowning. Layer retries — client retries × gateway retries × service retries — and the multiplication compounds. Retry storms have turned five-minute blips into multi-hour outages more times than the industry admits.
Discipline that prevents it:
- Retry budgets, not retry counts. Cap retries at ~10% of a client's request volume; past that, failures return immediately. A hard cap per request (3 attempts) plus exponential backoff with jitter — desynchronized retries so recovery is a ramp, not a synchronized stampede.
- Retry only what can succeed. 429/503-with-Retry-After means stop, honor the hint. Retrying an overload signal is aggression.
- Circuit breakers stop the flow entirely when failure rates spike, converting a hammering client into a polite periodic probe.
- No hedging into overload. Sending a backup request when p99 is slow is great for tail latency in a healthy system and gasoline in an overloaded one — hedge only under an explicit budget.
And a quiet trap on the recovery side: systems that fall over at 80% utilization because "we have autoscaling" forget that scale-out takes minutes while collapse takes seconds. Shedding is what buys autoscaling the time to matter. When capacity does return, drain the backlog with admission control still engaged — letting a full queue flood freshly recovered instances is how you get the double-dip outage.
Takeaways
Overload is not a bigger-instance problem; it is a decision problem — which work dies so the rest survives. Bound queues, reject fast and early, propagate deadlines and drop dead-on-arrival work. Tier requests by criticality and shed from the bottom using local load signals. Build degraded modes before you need them, and chain retries to budgets and breakers so your clients stop being their own DDoS. Goodput under stress is a design property — systems keep their promises during incidents only when someone decided, in advance, which promises those were.
Keep reading
Designing Chat Presence: Online, Away, and the Lie of Instant Status
Heartbeat vs subscriptions, fan-out of presence, privacy, and why a boolean online flag does not survive a million concurrent sockets.
Consistency Models Beyond CAP: Linearizability, Causal, and Session Guarantees
'Strong vs eventual' is a cartoon. The real spectrum — linearizable, sequential, causal, session guarantees — and how to pick per operation, not per system.
Designing an API Gateway: The Front Door as a System
Authentication, routing, rate limits, transformations, and a plugin chain — in a tier that must add ~1ms and never be the outage. Envoy/Kong architecture from scratch.
Designing a CDC Pipeline: Change Data Capture from Binlog to Downstream
Dual writes are a lie; the database's own log is the truth. Log-based CDC, the snapshot-plus-stream handoff, schema evolution, and ordering guarantees that survive resharding.
Designing a Content Moderation Pipeline: Trust and Safety at Platform Scale
A billion uploads a day, a legal requirement to act in hours, and irreversible mistakes in both directions: hash matching, classifier tiers, human review queues, and appeals.
Designing a Distributed Rate Limiter: Local Buckets, Global Truth
One user, twenty gateway nodes, one limit of 100 req/s. Where does the counter live? Redis+Lua atomicity, the local-cache compromise, and failing open vs closed.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.