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
Database Replication: Leader-Follower, Quorums, and the Lag Nobody Budgets For
Sync vs async replication, why failover loses writes, and how quorum systems skip the leader entirely. The trade-offs that set your durability story.
Sharding a Database: Partition Strategies That Survive Growth
Hash vs range partitioning, choosing a shard key you won't regret, the hot-partition problem, and why cross-shard queries are the real cost of sharding.
Designing a Distributed Job Scheduler That Doesn't Fire Twice (Usually)
Delayed jobs, cron at scale, and work claiming with SKIP LOCKED. Why every scheduler promises at-least-once and what that forces on your handlers.
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.