Concepts

Measuring systems

Foundations

Percentiles and Tail Latency

Why the average response time is the wrong number, and why the slowest 1% becomes everyone's problem once a request fans out to many services.

Every extra service a request touches raises the chance it meets a slow one. Ten dependencies at a 1% tail means one request in ten is slow.

p99percentilesfan-outhedged requests

Try it

Move the dials — the sentence under the picture changes.
One service: slow 1.0% of the timeaverage 24 ms · p99 400 msA request across 10 services: slow 9.6% of the time1 − (1 − 0.010)^10 = 0.096The request is as slow as its slowest reply. Fan-out turns a per-service tail into a per-request norm.
Each service answers in 20 ms except 1.0% of the time, when it takes 400 ms. Its average is a healthy 24 ms. But a request that waits for all 10 meets at least one slow reply 9.6% of the time.

In plain words

If you time a thousand requests and sort them, the p99 is the 990th: 99% of requests were at least this fast. The p50 is the median, the middle one. The average is neither, and it hides the thing users complain about — the slow ones. The slowest 1% is called the tail, and in a big system the tail is not rare at all, because one user's page is built from dozens of requests and any one of them can be the slow one.

Percentiles, not averages

half of requests are faster — 'typical'
p50
1 in 20 is slower than this
p95
1 in 100 — what a busy user hits daily
p99
1 in 1,000 — a big customer hits it hourly
p99.9

A user who loads 100 pages a day meets your p99 every day. A customer whose script makes a million calls a day lives at your p99.9. The average describes nobody in particular.

The tail at scale

A request that fans out to N services and waits for all of them is as slow as the slowest. If each service is slow with probability p, the request meets at least one slow reply with probability 1 − (1 − p)ᴺ.

s1s2s3s4s5s6s7s8s9s10A request fans out to 10 services. Each is slow 1% of the time.Request 1: s8 was slow. The whole request waits for it.Request 2: all fast.Request 3: s3 was slow.1 − 0.99¹⁰ = 9.6% of requests meet a slow reply. At 100 services it is 63%.
Rare per service, common per request. The maths is unforgiving and the widget lets you feel it.

Where tails come from

  • Garbage collection pauses, JIT warm-up, a page fault
  • Queueing — a burst arrived just before you; see utilisation and queueing
  • Cache misses — 1% of reads go to disk, and disk is 100× memory
  • Noisy neighbours — another tenant on the same host is busy
  • Retries and timeouts — a request that waited 2 s for a timeout, then succeeded on the retry, counts as 2 s
  • The network — a lost packet is a retransmit timeout of 200 ms+

Most are not bugs. They are the normal behaviour of machines, which is why the tail cannot be fixed only by making code faster.

Cutting the tail

Hedged requests
the classic

Send the request; if no reply within the p95, send the same request to a second copy and take whichever answers first. Costs ~5% extra load, removes most of the tail. Only for idempotent reads.

Tied requests

Send to two copies at once, each told about the other; whichever starts first cancels the other. Less waste than hedging, needs cooperative servers.

Do not wait for everyone

Search 30 shards, return after 28 reply. Good enough results in a fraction of the time. Only when partial answers are acceptable.

Shrink the fan-out

Batch the calls, cache the results, denormalise. Every service removed from the path takes a term out of 1 − (1 − p)ᴺ.

Timeouts and budgets

Give the whole request one deadline and pass what is left downstream. A slow shard should cost the user 300 ms, not 30 s.

A hedged read: second attempt after the p95, first answer winsTypeScript
async function hedged<T>(call: () => Promise<T>, hedgeAfterMs = 50): Promise<T> {
  const first = call();
  const second = new Promise<T>((resolve, reject) => {
    const t = setTimeout(() => call().then(resolve, reject), hedgeAfterMs);
    first.finally(() => clearTimeout(t));       // first answered in time: never send the hedge
  });
  return Promise.race([first, second]);         // whichever comes back first
}

Where it goes wrong

  • Measuring p99 per service and assuming it composes. It does not. The request's p99 is far worse than any one service's. Measure end to end.
  • Hedging writes. Two copies of "charge the card" is two charges. Hedge only idempotent requests.
  • Averaging percentiles. The mean of ten servers' p99s is not the fleet's p99. Compute percentiles from the raw distribution, or use a histogram that merges.
  • Setting the timeout at the average. A 50 ms timeout on a 20 ms service with a 400 ms tail turns every tail event into an error. Set it from the tail you are willing to wait for.

Take this with you

  • The one idea: averages hide the tail, and fan-out turns a rare tail per service into a common one per request.
  • In an interview, report percentiles, do the 1 − (1 − p)ᴺ arithmetic, and name hedged requests for idempotent reads.
  • At work, find your widest fan-out and measure its p99 end to end. That is the number to work on, not any single service's.