CS PAPERS DEEP-READ · PAPER 27
Dean & Barroso · Google · CACM 2013
In 2013, Google's Jeff Dean and Luiz Barroso wrote a short article — a few pages — called "The Tail at Scale," about something nobody running a large website can dodge: why a system that is "fast on average" still hands users visible stalls. Their answer isn't "make every machine faster" — you can't — it's to admit machines will occasionally be slow, then let the software absorb that slowness.
Suppose your machines are good citizens: each answers within a second 99% of the time and only dozes off 1% of the time. But one modern web search must ask a hundred machines at once and wait for all of them before it can assemble a result. And then — more than six out of ten requests take over a second. Every machine scores "99 out of 100," yet together they fail six times in ten.
The reason is plain: you're not waiting on the average, you're waiting on the slowest one. One person can always be punctual; a team of twenty almost never arrives together — one latecomer and everyone waits. Scale doesn't add up the good luck, it amplifies the bad luck.
And that "dozing off" isn't a machine breaking. It's a machine with other things on its plate: someone else's program on the same box is grabbing CPU and memory; a background chore occasionally tidies up accumulated data; memory garbage has to be collected, and collecting means pausing the real work to sweep; a machine that runs hot throttles itself; and requests queue up layer after layer. Each of these only costs a few milliseconds now and then — but if you're waiting on hundreds of machines at once, one of them is always mid-nap.
Engineering went through an important change of mindset once: we stopped hoping machines wouldn't break, assumed they would, and kept spare copies so the system keeps working when one dies — that's fault tolerance. This article says: do the same thing for slowness.
The most direct move is a backup request: after sending a request to one machine, if it hasn't answered in a while, ask a second machine that holds the same data, use whichever answers first, and cancel the other. The beauty is how cheap it is — only that small minority of slow requests ever triggers a second ask, so you send well under a few percent extra traffic, while the slowest sliver of requests drops from "a second or two" to "tens of milliseconds." The other move is to dodge in advance: if a machine has been visibly sluggish lately, pull it off the roster and watch it for a while — drop one slow machine and the whole thing gets faster.
For unavoidable chores like background cleanup, intuition says "stagger them, don't let them collide." The article says the opposite: make all the machines do it at the same time. Once you see it, it's obvious — if they're staggered, at any given moment some machine is busy sweeping, and since you always wait for all of them, every single request gets dragged; if they all sweep together, only the requests in that one short window suffer and the rest of the time is spotless.
An honest note: most of these moves buy stability with slack — asking twice and keeping spare replicas both require idle capacity. On a cluster already running flat out, the extra requests will drag everyone down instead.
Across hundreds or thousands of machines you wait on the slowest one, so "occasionally slow" gets amplified by scale into "often slow." Since you can't cure it, attack "machines get slow" the way we once attacked "machines break": ask a second machine, take whoever's first, bunch the chores into one moment, pull the slow machines out — teach the system to tolerate the tail.
Want the amplification curve, the hedged/tied request timelines, and the measured numbers? → switch to the deep read
In a system where hundreds or thousands of machines cooperate on one request, occasional per-machine latency variability gets amplified by scale into stalls users can feel — because response time is set by the slowest component. The sources of that variability (resource contention, background maintenance, garbage collection, queueing, power throttling) can't be eliminated, so we should do for latency what fault tolerance once did for reliability: assemble a predictably responsive service out of unpredictable components using a set of tail-tolerant techniques. It also shifted the industry habit from "watch the average latency" to "watch the 99th / 99.9th percentile."
The authors are Jeffrey Dean and Luiz André Barroso, two central figures in Google's infrastructure (Dean co-authored MapReduce, Bigtable, and Spanner; Barroso championed the "datacenter as a computer" view), published in Communications of the ACM in 2013. It isn't a research paper with a new algorithm but a distilled-experience perspective piece, compressing a decade of running hyperscale online services into one framework. It builds on MapReduce's (2004) backup tasks — re-launching a straggler task to race the original, the seed of "hedging" — and on Dapper (2010, Paper 26): only once you can see a whole call chain per request can you manage its tail.
Start with the arithmetic that stunned everyone. Suppose a server has a 1% chance of taking more than a second — a perfectly healthy-sounding number. But if a user request must ask 100 such servers in parallel and wait for all of them, then 63% of user requests exceed one second (the chance that not one machine dozes is only 0.99¹⁰⁰ ≈ 37% — "every machine goes smoothly" has to hit 100 times in a row, which is hard). Component-level rare events get amplified by fan-out width into a system-level norm.
The article also reports measurements from a real Google service: for one random leaf request the 99th-percentile latency is about 10 ms; "wait for all leaves" is about 140 ms at the 99th percentile; waiting for only 95% of the leaves brings it down to roughly 70 ms — same machines, and just changing "how many you wait for" moves user-visible latency several fold.
The key insight: this slowness is mostly not failure but a by-product of normal operation, so no amount of "fixing it" will remove it — shared-resource contention (co-located applications competing for CPU cores, caches, memory and network bandwidth), background daemons (cheap on average, but a few milliseconds of hiccup when scheduled), global resource sharing (switches, shared file systems), maintenance activities (log compaction, index updates, data rebalancing), garbage-collection pauses, multi-layer queueing (NIC, OS, thread pools — each widens the distribution and can block the head of line), and power and thermal limits (throttling at the power cap). And hardware trends — more cores, more aggressive power saving and sharing — only widen the variability.
The most valuable idea in the piece is an analogy: engineering long ago stopped chasing "components that never fail" and instead assumed failure, building reliable systems out of redundancy — fault tolerance. Here: do that for latency. Treat "this component is occasionally slow" as a transient fault the software routes around at runtime, and you can assemble a predictably responsive service from components whose response times are unpredictable. That stance is called tail-tolerant, and it plays out two ways: shrink the variability, and live with it.
① Differentiate service classes and queue high. Give interactive requests and offline batch work different priorities, keep the queues in the high layers close to the application and the low-level device queues short — the shorter the queue, the more say your scheduling policy has. ② Break head-of-line blocking. Slice an expensive request into a series of small ones executed interleaved, so it can't dam up the millisecond-scale requests behind it.
③ Manage background activity — and synchronize the disruption. Throttle background tasks and push them toward low-load periods; the counterintuitive part is the second half: make the cluster's heavyweight background work happen at the same time everywhere, rather than staggered. Because when you wait for all the leaves, staggering means some machine is busy at every instant, so every request gets dragged; synchronizing compresses the damage into one short window and leaves the rest of the time clean.
An interactive request lives only tens of milliseconds — too short to observe and adapt — so the slowness must be routed around inside this one request. The core move is asking redundantly, but asking cleverly: sending every request to every replica would indeed take the fastest answer, at multiplied cost.
Hedged requests. Send to one replica first; if it hasn't answered past the time it normally would (the paper's threshold is the 95th percentile of expected latency), send a second copy to another replica, take whichever answers first and cancel the other. Why it's cheap: only the tail 5% of requests ever trigger the second ask, so extra traffic is a few percent while covering exactly the painful part. Measured: reading 1,000 keys from a BigTable-like table spread over 100 machines, hedging after a 10 ms delay cut the 99.9th-percentile latency from 1,800 ms to 74 ms, while sending only 2% more requests.
Tied requests. Hedging's flaw is that the waiting window itself is wasted. Tied requests go further: enqueue the request on two replicas at the same time and tell each machine who its twin is; whichever dequeues it and starts executing immediately sends the other a cancellation. So you're not waiting on a timeout but on "who reaches the head of the queue first" — consuming the queueing difference between machines directly. One key detail: give the second machine a tiny delay (about twice the average one-way network latency, say 1 ms) so both don't start at once and duplicate the work. The article reports it clearly lowers both median and 99.9th-percentile latency, and still helps when a large sort job is competing for the same disks.
Some imbalances last seconds to minutes (a machine runs hot, load skews), so they can be tuned across requests:
"Good enough": search has no single correct answer, so time can be a hard constraint — at the deadline, assemble the answer from whatever has arrived instead of waiting for straggling leaves, and if needed skip non-essential subsystems (spelling correction, ads) to buy time, trading a little result quality for predictable latency. Canary requests: one weird request hitting a never-exercised code path can crash or pathologically slow every leaf, so send it to one or two leaves first and only fan out to the rest if that returns normally — one extra hop of latency instead of a whole-fleet wipeout.
Most of these techniques require operations to be read-only or idempotent — repeating a read is harmless, repeating a transfer is not. Fortunately updates tend to be less tail-sensitive: many can be moved off the critical path and done asynchronously, many services tolerate a brief inconsistency window, and the quorum algorithms used when strong consistency is required are inherently tail-tolerant, committing as soon as a majority acknowledges.
This is a perspective piece, so the "results" aren't benchmark scores but three pieces of production evidence: ① the amplification arithmetic and measurements (1% → 63%; one leaf at about 10 ms vs about 140 ms waiting for all, at the 99th percentile); ② hedged requests cutting the 99.9th percentile from 1,800 ms to 74 ms for only 2% extra requests; ③ tied requests lowering median and 99.9th-percentile latency both on an idle cluster and under disk contention. Together they support the thesis: tail latency is measurable and budgetable, and a small amount of redundancy buys a large improvement.
It first changed what we measure. Service performance used to be written down as "average response time"; this article got the industry to accept that the average carries almost no information and you must watch p99 / p99.9, and to write that into service-level objectives (SLOs). Every p99 curve on today's dashboards traces back here.
Next, a whole toolkit went mainstream: hedged and tied requests became standard parts (gRPC ships hedged retry policies, Cassandra has speculative retry, MapReduce- and HDFS-style speculative execution shares the same root); "latency-induced probation" lives on in service meshes as outlier detection and ejection in implementations like Envoy; micro-partitioning is routine for load balancing in modern sharded systems; "good enough" is standard practice in search, recommendation and feed systems. More broadly, together with Dapper (which made it visible) it established latency engineering as a craft with a method: measure the distribution rather than the mean, decide whether the variability is source-side or amplified, then pick from "shrink it" versus "tolerate it."
① In one line: under wide fan-out, response time is set by the slowest component, so occasional per-machine variability becomes a permanent stall; build "tail-tolerant" software the way we build fault-tolerant software.
② Amplification: 1% chance of exceeding a second per machine, waiting on 100 in parallel, means 63% of requests exceed a second; measured, one leaf is about 10 ms at p99 versus about 140 ms waiting for all.
③ Sources of variability: resource contention, background daemons, global sharing, maintenance activity, garbage collection, multi-layer queueing, power throttling — mostly by-products of normal operation, not removable.
④ Shrinking it: service classes plus short high-level queues; slicing long requests to break head-of-line blocking; throttling background work and synchronizing the disruption (aligned beats staggered, because you always wait for every machine).
⑤ Within-request: hedged requests (send a second copy past the 95th percentile of expected latency, take whoever's first) and tied requests (enqueue on two, first to start cancels the other, consuming the queueing difference). Key number: reading 1,000 keys, hedging after 10 ms took p99.9 from 1,800 ms to 74 ms for 2% extra requests.
⑥ Cross-request and retrieval specialties: micro-partitions, selective replication of hot partitions, latency-induced probation (eject the slow machine — fewer machines, faster service), good enough, and canary requests; writes are less urgent because they can go async, tolerate brief inconsistency, and commit on a quorum.
⑦ Limitations: experience rather than theory; it buys stability with slack (retries can backfire under load); it depends on idempotence and deliverable cancellation; and it masks the symptom rather than curing the source.