BOOK DEEP-READ · SRE · CHAPTER 21
Site Reliability Engineering · Ch 21 · Alejandro Forero Cuervo · Google · 2016
You have fought for concert tickets, refreshed a doctor's booking page, hit "buy" the second a sale opened. What happens on the servers in that instant is what this chapter is about: more people show up than the place can serve. Chapter 21 of Google's SRE book is not about making systems faster. It asks a more practical question: once it already cannot keep up, then what?
Picture a restaurant with 20 tables. Tonight 200 people arrive. The worst thing you can do is let them all in. Everyone crowds inside, the waiters run themselves ragged, orders pile into a tangle, and two hours later not a single table has been served a complete meal. All 200 people go hungry. The restaurant did not "serve 200 people" — it just ruined the 20 tables it could have served properly.
A system's instinct is to accept everything. Requests come in and queue up, the queue grows, everyone waits longer. Waiting long enough, the caller assumes failure and hits refresh — so the same crowd comes back as two crowds, then three. The pressure starts feeding itself, and within tens of seconds you slide from "a bit slow" to "no response at all."
The frightening part is not slowness. It is that useful output drops to zero: the machines run flat out, spending every cycle computing results nobody is waiting for anymore.
Four moves, all of them versions of gracefully doing less:
It turns "no" into a real capability rather than a sign of failure. By admitting the ceiling exists and deliberately taking on less, the system holds its output steady at that ceiling when traffic spikes, instead of falling off a cliff at some point. The cost is honest too: saying no takes staff of its own (somebody has to stand at that door), and the short menu is almost never cooked, so on the night you need it, it may well not come out of the kitchen.
The most expensive mistake under overload is trying to accept everything. Rather than straining until the whole thing collapses and nobody gets anything, say no earlier and selectively — so that the people you did keep actually walk away with something.
Want the mechanisms, the formula and the diagrams? → Switch to the deep read
Overload was never about whether you can take it — it is about what happens in the moment you can't. This chapter's claim is mildly counterintuitive: a healthy service must be able to actively refuse some requests, because accepting everything indiscriminately does not end in "everyone is a bit slower," it ends with goodput collapsing to near zero. Around that claim it gives four separately implementable moves: measure capacity in resources rather than QPS; let the server degrade and shed; classify requests by criticality in advance; and make clients throttle themselves and put three gates on retries.
This chapter comes from Part III ("Practices") of the SRE book, written by Alejandro Forero Cuervo. It follows directly on the two load-balancing chapters — those explain how to spread load evenly so you avoid overload; this one covers what to do when you are overloaded anyway — and leads into Chapter 22, "Addressing Cascading Failures." This chapter is a single service defending itself; the next one is what the whole system looks like after that defense fails. In the real world it maps onto every flash sale, every viral event, every avalanche triggered by one slow dependency.
Start with the counterintuitive part: a service's genuinely dangerous window is not "slow" — it is the thirty seconds after slow.
Watch what happens with no defenses. A service rated for roughly 10k QPS gets pushed to 15k. Requests queue; as the queue grows, response time goes from 50 ms to seconds. In-flight requests pile up in memory, GC pressure rises and eats CPU in turn. Clients set a 1-second timeout, don't get an answer, and retry — so real demand of 15k arrives as 30k, then 45k. And while those retries land, the queue is still faithfully processing requests that timed out long ago and that nobody is waiting for. At that point CPU is pinned, every cycle is spent on results no one wants, and goodput is near zero.
That is not "not fast enough." It is structural collapse: load has started feeding itself. So the problem the chapter takes on is — when load exceeds capacity, how do you make output flatten at the capacity line instead of falling off a cliff? It splits the answer in two: the server must be able to do less on purpose, and the client must be willing to send less on purpose. You need both, because a rejected request that immediately comes straight back still costs the server CPU to reject.
The usual way to state capacity is "this service handles X QPS." The chapter says flatly that this is a bad ruler. Request costs can differ by one or two orders of magnitude — a primary-key read of 3 rows and an aggregation that scans 500 shards both count as "1 query" but consume nothing alike. Worse, the traffic mix drifts as features launch and the customer base changes, so the ceiling you measured yesterday no longer holds today.
Google's approach is to define capacity directly in resource consumption, and in the overwhelming majority of cases that means watching CPU. Two reasons. First, in a garbage-collected runtime, memory pressure naturally translates into CPU consumption (tight memory → more frequent GC → more CPU burned), so watching CPU watches memory for free. Second, the remaining resources can be provisioned generously enough that they are very unlikely to run out before CPU does. CPU also has one crucial property — it is a compressible resource. Running short only makes requests slower; it does not kill the process outright the way exhausting memory does. That is precisely the window a shedding mechanism needs in order to react.
The concrete signal is executor load average: an exponentially decayed average of the number of active threads in the process, compared against the number of available CPUs, giving a continuous reading of "how full am I right now?"
The server has two roads under overload, and they are qualitatively different. Graceful degradation makes the answer cheaper: search only a small in-memory slice of the index instead of the full on-disk one, swap ranking for a simpler algorithm, fall back from personalized recommendations to a static popularity list. The request is still served, just one notch worse. Load shedding declines to serve at all: return an error (usually 503 over HTTP) and keep the resources for the requests you can still serve properly.
On degradation the book offers a notably restrained warning, worth memorizing on its own: the degraded path is almost never executed, so it is very likely already broken. On the day you finally switch to it, you discover that code drifted away from the main path months ago, or does not run at all. The counter is: keep the number of modes small, keep the logic simple, and actually exercise it on a schedule.
A shared service's capacity has to be divided among many customers, so you start with per-customer quotas — allocated as a resource rate (CPU-seconds per second, say) rather than as QPS. The quotas usually deliberately sum to more than total capacity, betting that customers won't all max out at once; when they do, the ones over quota get rejected first.
But there is a problem you cannot design around: rejecting isn't free. One furiously retrying customer can consume the server's entire CPU budget generating rejection responses. So the chapter's prettiest move lands on the client: adaptive throttling. Each client task keeps two numbers over a sliding 2-minute window: requests (how many the application layer issued) and accepts (how many the backend actually took). Normally the two track each other closely; once the backend starts rejecting, accepts falls behind. The client then drops a fraction of requests locally, probabilistically, without ever putting them on the wire:
In plain words: when what you send exceeds what gets accepted by too much, refuse that excess proportion yourself first. K is typically 2 — "I may send twice what the backend accepts." Smaller K is more aggressive and self-limits earlier; larger K is more permissive. The + 1 in the denominator merely avoids dividing by zero.
Two design details are the real substance. First, the probability never reaches 1: the client always leaks a trickle of requests through, which is its only way of detecting whether the backend has recovered. When accepts climbs back, the throttle releases itself — nobody has to clear it by hand. Second, locally rejected requests still count toward requests, so the client cannot fool itself into believing the pressure has gone away.
Shedding means dropping some requests — but which? The chapter's answer: don't improvise in the middle of an overload; classify requests up front. Google's RPC system has four levels, highest to lowest:
CRITICAL_PLUS — failure causes serious, user-visible impact.CRITICAL — the default for requests issued by production jobs; user-visible impact, but less severe. Capacity planning must cover the sum of these top two levels.SHEDDABLE_PLUS — partial unavailability is acceptable; the default for batch jobs, which can retry minutes or hours later.SHEDDABLE — frequent partial unavailability and occasional full unavailability are both acceptable.Two mechanisms make it genuinely usable. First, criticality propagates down the call chain automatically: a service that receives a CRITICAL request issues its own downstream requests as CRITICAL by default — no layer has to guess. Second, quotas are accounted per criticality: a customer's CRITICAL quota and its SHEDDABLE quota are separate books, so low-value traffic cannot eat the high-value allowance. When shedding, a task always cuts from the lowest criticality upward.
The most dangerous positive feedback loop under overload comes from retries. The chapter offers one piece of arithmetic worth internalizing: three attempts per layer, stacked three layers deep, means the bottom layer receives 3³ = 27× the traffic — and that bottom layer was already failing because it couldn't cope. You think retries are raising your success rate; in fact you are pouring fuel on a machine that is already on fire.
Three gates push it back down:
There is one more rule of judgement: whether the overload is local or widespread decides whether retrying elsewhere is worth it. If only a few tasks are overloaded, immediately retrying against a healthy task is right. If a whole datacenter is broadly overloaded, retrying just relocates the problem — the correct move is to let the error bubble all the way up to the caller and let it decide between degrading and giving up.
One last thing that is easy to miss: merely maintaining connections costs money. Health checks, keepalives, connection setup and teardown can outweigh the actual request handling once the client count gets large. The classic case is a batch job that spins up thousands of workers, each connecting to the same backend — request volume is modest, but connection overhead flattens the backend first. The fix is to funnel such clients through a small set of proxy tasks, dropping the backend-facing connection count from "number of workers" to "number of proxies."
Table 1 · Which signal tells you "I am overloaded"
| Signal | Strengths | Cost / where it breaks | Who uses it |
|---|---|---|---|
| QPS / request count | Intuitive, cheap to collect, easy to talk about | Completely distorted the moment request costs differ; any shift in traffic mix forces a re-measure | Homogeneous simple services, coarse rate limits |
| CPU utilization | Broad coverage; GC makes memory pressure show up as CPU; CPU is compressible, leaving reaction time | Fails on IO-bound services — CPU sits idle while the downstream database is the bottleneck | This chapter's default (Google) |
| Concurrency / queue depth | Maps directly onto queueing, reacts fast | Thresholds are hard to pick; a slow downstream naturally inflates concurrency and reads as self-overload | Netflix concurrency-limits |
| Response latency | Closest to what users feel; sensitive to a slowing downstream | A lagging indicator — by the time it climbs, the queue has usually been building for a while | Netflix per-endpoint target latency |
Table 2 · The server's four possible reactions to overload
| Approach | What users see | Cost | When to use it |
|---|---|---|---|
| Push through (no defenses) | Very slow, then blanket timeouts | goodput collapses to ~0, and it spreads along the call chain | Should never be the default behavior |
| Graceful degradation | Lower-quality results (less data, rougher ranking) | The degraded path is never exercised and rots easily; also complicates production debugging | Read paths that have a natural "cheap answer" |
| Load shedding | Some callers get a clear, fast error; the rest are served normally | Rejecting still burns CPU; a bad experience for whoever got rejected | The general backstop — cheaper the further out you do it |
| Autoscaling | Brief slowdown, then recovery | Minutes of lag, useless against second-scale spikes; new instances need warm-up and may worsen downstream load | Predictable, trend-driven growth |
Table 3 · What each of the three retry gates actually stops
| Gate | Rule | Stops | Limitation |
|---|---|---|---|
| Per-request limit | At most 3 attempts for one request | A single request grinding forever in one layer | Does nothing about multiplication across layers |
| Per-client budget | retries / requests ≤ 10% | Pins total request amplification near 1.1× | Requires honest client accounting; uncontrolled third-party clients ignore it |
| "Don't retry" marker | The backend states it is overloaded; upstreams stop retrying at once | The cross-layer 3ⁿ multiplication | Every framework on the path must understand the semantics |
A decade on, this chapter is still the common basis for rate-limiting, circuit-breaking and degradation designs, because it decomposes overload handling into four separately implementable moves: what to measure, how the server does less, how requests get ranked, and how the client sends less. The adaptive concurrency filter you see in Envoy/Istio, the breakers and fallbacks in Hystrix/Sentinel, the retry token buckets in every vendor SDK — all of them are re-implementations of this material. The good answer to "what do you do when a service is overloaded?" in an interview is these four layers, not "add machines."
5:06am PDT they paused requests to the metadata service, which decreased retry activity and finally relieved the load — proving the point of the third gate: retries must be interruptible on purpose.AWS, "Summary of the Amazon DynamoDB Service Disruption," 2015 ↗① In one line: overload isn't about whether you can take it, but whether you can lose only part of it. Undifferentiated pushing through ends with goodput near zero.
② Don't measure capacity in QPS — request costs vary too much. Measure resources; Google's default is CPU (GC turns memory pressure into CPU; CPU is compressible, leaving reaction time), signalled by executor load average.
③ Two server-side roads: graceful degradation (a cheaper answer) and load shedding (refusing outright). The degraded path never runs and rots easily, so keep modes few, simple, and rehearsed.
④ Per-customer quotas are allocated as resource rates and may deliberately oversubscribe; but rejecting costs CPU too, so it only closes the loop with client-side throttling.
⑤ Adaptive throttling: the client tracks requests and accepts over a 2-minute window and drops locally with probability max(0,(requests−K×accepts)/(requests+1)), K typically 2. The probability never reaches 1, so a trickle always leaks through to detect recovery.
⑥ Four criticality levels (CRITICAL_PLUS / CRITICAL / SHEDDABLE_PLUS / SHEDDABLE), propagated automatically down the RPC chain with per-level quotas; capacity planning covers the top two; shedding cuts from the bottom up.
⑦ Three retry gates: 3 attempts per request, retries ≤ 10% of requests per client (pinning amplification near 1.1×), and an "overloaded, don't retry" response so the signal travels up — otherwise three layers of three attempts is 27×.
⑧ Only local overload justifies retrying against another task; when a whole datacenter is overloaded, retrying just relocates the problem, so let the error bubble up.
⑨ Connections are load: thousands of batch workers connecting directly can cost more than the requests themselves — funnel them through a small proxy tier.
⑩ In practice: Amazon sheds at every layer and implements retry budgets as token buckets; Netflix put prioritized shedding in Envoy and switched IO-bound services to latency signals; Stripe reserves 20% of capacity for critical APIs.