Skip to content

Percentiles & Tail Latency

The BoxOffice dashboard says the seat-map endpoint has an average latency of 55 ms. Everyone is happy β€” except the fans, who complain on social media that the seat map "takes forever" during on-sales. Both are right. The average is 55 ms; but one request in a hundred takes more than half a second, and a fan who opens the seat map, refreshes it, zooms into two sections and tries three holds makes enough requests to hit one of those slow ones almost every visit. This page is about the slow requests: how to measure them, why they matter more than the average suggests, and why they get worse as systems become more distributed.


Why averages lie

Latency distributions are not bell curves. Most requests are fast and similar; a minority β€” hitting a cache miss, a garbage-collection pause, a lock, a retry, a cold connection β€” are much slower. The result is a long tail to the right. The plot shows a synthetic but realistic distribution for 100,000 requests: 97% are "normal" (around 40 ms) and 3% hit a slow path (around 400 ms):

2026-11-01T21:54:29.183916 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

The mean (about 55 ms) describes no actual request: it is pulled up by the tail, yet it hides how bad the tail is. The median says what a typical request experiences. The p99 and p99.9 say what the unlucky requests experience β€” and they are roughly 12Γ— and 28Γ— slower than the median (β‰ˆ 510 ms and β‰ˆ 1.1 s vs β‰ˆ 40 ms). A dashboard that shows only the average would never reveal that.


Percentiles

The p-th percentile (written pXX) of a set of latencies is the value below which XX% of them fall1:

Percentile Meaning Also called What it tells you
p50 half the requests are faster median the typical experience
p90, p95 90% / 95% are faster β€” the common "slow-ish" experience
p99 99% are faster; 1 in 100 is slower "two nines" tail the experience of the unlucky β€” and of heavy users
p99.9 1 in 1,000 is slower "three nines" tail rare but real; dominates when requests fan out
max the slowest one seen p100 outliers; noisy, but worth a look

To compute a percentile exactly, sort the values and take the one at position ⌈p Γ— nβŒ‰ (the nearest-rank method): with 1,000 requests, p99 is the 990th fastest. In production, systems do not store every latency; they record histograms β€” counts of requests per latency bucket (0–5 ms, 5–10 ms, …) β€” and estimate percentiles from the buckets. Prometheus histograms, HdrHistogram and OpenTelemetry exponential histograms all work this way (class 9).

You cannot average percentiles

If server A has p99 = 100 ms and server B has p99 = 1,000 ms, the p99 of the fleet is not 550 ms; it depends on how many requests each served and on the shape of both distributions. The same is true across time: the average of 60 per-minute p99 values is not the p99 of the hour. To aggregate, merge the histograms (add the bucket counts) and compute the percentile from the merged histogram. Dashboards that average percentiles are among the most common β€” and most misleading β€” mistakes in monitoring.

Why the tail matters more than "1%" suggests

  1. Users make many requests. A fan's visit during an on-sale (event page, seat map, refreshes, holds, payment, confirmation) easily involves 50 requests. The probability that at least one is slower than the p99 is \(1 - 0.99^{50} \approx 39\%\). The "1% tail" is felt by four in ten visits.
  2. The slowest users are often the best customers. Fans with many orders, organizers with large events, accounts with a lot of data β€” their requests do more work and land in the tail. Amazon famously set its internal latency targets at the p99.9 for this reason2.
  3. The tail is where overload starts. As the queueing curves show, the p99 rises long before the mean when a component approaches saturation. It is the earliest warning you get.

Tail amplification under fan-out

Modern backends rarely answer a request with a single call. The BoxOffice seat-map page calls inventory (availability), catalog (venue layout and prices), order (the fan's existing tickets) and a recommendations service, in parallel; the search service queries 20 index shards in parallel and merges the results. A request that fans out to N backends in parallel is as slow as the slowest of them.

If each backend call is slow (above its p99) with probability q = 1%, independently, the probability that the user request waits for at least one slow call is:

\[ P(\text{user request is slow}) = 1 - (1 - q)^N \]
Fan-out N q = 1% (backend p99) q = 0.1% (backend p99.9)
1 1% 0.1%
5 4.9% 0.5%
20 18% 2%
100 63% 9.5%
500 99% 39%
2026-11-01T21:54:29.381009 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

This is the central observation of Dean and Barroso's The Tail at Scale3: with a fan-out of 100, a backend whose p99 is 1 second makes 63% of user requests take more than 1 second. In a large distributed system, the backend's tail becomes the user's median. The more you split a system into parallel pieces (shards, microservices), the more the rare slowness of each piece dominates.

BoxOffice β€” search across 20 shards

The search service splits the event index into 20 shards to keep each one small and fast. Each shard answers in 15 ms at p50 and 120 ms at p99 (a merge or a cache miss now and then). The user's search waits for all 20:

  • P(at least one shard above 120 ms) = 1 βˆ’ 0.99²⁰ β‰ˆ 18% β€” so the user-facing p82 is already β‰₯ 120 ms, and the user-facing p99 is driven by the shards' p99.9 or worse.
  • Halving the number of shards to 10 cuts that probability to ~10%, but makes each shard bigger and slower. Keeping 20 shards and hedging (below) is usually the better trade.

Where the tail comes from

Tail latency is rarely caused by the "normal" code path. Typical causes3:

Source Example Mitigation
Queueing a burst arrives while a server is busy headroom (lower utilization), load balancing on least-loaded
Shared resources noisy neighbours on the same host, disk or network isolation, dedicated capacity for critical paths
Garbage collection a JVM pauses for 200 ms GC tuning, low-pause collectors, smaller heaps
Background work database compaction, log rotation, backups schedule off-peak, rate-limit background tasks
Cache misses cold cache after deploy; rare keys pre-warming, larger cache, request coalescing
Retries and timeouts the first attempt times out at 1 s, the retry succeeds well-tuned timeouts, hedging instead of waiting
Cold starts a new container or serverless function initializes warm pools, provisioned concurrency
Slow dependencies the payment provider has a bad minute timeouts, circuit breakers, async processing

Taming the tail

You cannot eliminate variability; you can design so that it does not reach the user.

  • Hedged requests: send the request to one replica; if it has not answered within, say, the p95 latency, send a second copy to another replica and use whichever answers first. Only ~5% of requests are duplicated, but the tail shrinks dramatically. Dean and Barroso report a Google case where hedging after 10 ms cut the p99.9 of a 1,000-key read from 1,800 ms to 74 ms with only 2% more requests3. Hedge only idempotent reads.
  • Tied requests: send to two replicas at once, each knowing about the other; the first to start processing cancels the other. Removes queueing variability at the cost of a little coordination.
  • Reduce fan-out: fewer, larger partitions for latency-critical paths; precompute aggregates (a materialized seat-map summary instead of calling five services).
  • Degrade gracefully: give each backend a deadline and return a partial answer β€” the seat map without the recommendations panel is still a seat map.
  • Take work off the critical path: what does not need to finish before the response (e-mails, analytics, search indexing) goes to a queue.
  • Keep utilization low on latency-critical components, and isolate them from batch work.

Latency budgets

A user-facing target must be split across the components on the path. For BoxOffice checkout (p99 < 1.5 s):

Step Budget (p99) Notes
Edge (CDN, WAF, gateway) 50 ms TLS termination near the user
order service logic 50 ms validation, idempotency check
inventory (confirm hold) 100 ms one transaction on the primary
payment β†’ provider 1,000 ms the provider dominates; timeout at 1.2 s
Write order + outbox 50 ms one transaction
Slack 250 ms network variance, retries
Total 1,500 ms β€”

Summing p99s is pessimistic (they rarely all happen on the same request), which is exactly what you want in a budget. When a step exceeds its budget, you know whose problem it is. Each budget also sets that component's timeout β€” a timeout longer than the caller's whole budget protects nothing.


Measuring percentiles honestly

Two traps make tail latency look better than it is:

  • Coordinated omission: a load generator that sends the next request only after the previous one returns stops sending when the server stalls β€” so it never records the requests that would have arrived during the stall, and the tail disappears from the results. Use tools that keep a constant arrival rate (open model: k6's constant-arrival-rate, wrk2) and compare intended vs actual send times4. Class 15 covers it in depth.
  • Measuring at the wrong place: the server's own timer misses the time spent in the accept queue, the load balancer and the network. Measure where the user is (client-side, or at the edge), and compare with server-side numbers.

In the BoxOffice code: percentiles in the load test (lab 2)

The load test of Hands-on 2 avoids both traps. It uses k6's constant-arrival-rate executor (an open model: new requests keep arriving at RATE per second however slow the service gets), measures at the client, and states its pass criteria as percentiles per endpoint, because a fast detail page and a slow city listing averaged together describe neither. The lab's example run shows why the tail is the number to watch: at 400 req/s the median was 1.5 ms while p99 was 224 ms β€” the average would have hidden that one fan in a hundred waited 150 times longer than the typical one.

load/load.js (open model, percentile thresholds)
export const options = {
  scenarios: {
    open: {
      executor: 'constant-arrival-rate',
      rate: RATE,
      timeUnit: '1s',
      duration: DURATION,
      preAllocatedVUs: Math.max(20, RATE),   // VUs ready to start requests
      maxVUs: Math.max(200, RATE * 10),      // k6 may add more if requests get slow
    },
  },
  summaryTrendStats: ['avg', 'med', 'p(95)', 'p(99)', 'max'],
  thresholds: {
    http_req_failed: ['rate<0.01'],
    'http_req_duration{name:detail}': ['p(99)<500'],
    'http_req_duration{name:list}': ['p(99)<1000'],
  },
};

Try it: in the scale-out scenario, run the load profile and note the p50 and p99. Then use chaos at low to make one replica occasionally slow. Which percentile moves first? By how much does the p99 move compared with the p50?

Think first: SLO for the seat map

BoxOffice wants an SLO for the seat-map endpoint. Option A: "average latency < 100 ms". Option B: "99% of requests < 300 ms, measured at the gateway over 28 days". Which one, and why?

Answer: B. It is stated as a percentile (it constrains the tail, which users feel), it has a threshold that matters to users, a measurement point (the gateway, closer to the user than the service) and a window. Option A can be met while 3% of requests take more than a second β€” exactly the complaint on social media. SLOs are developed in class 9.

In the interview

Always state latency targets as percentiles ("p99 < 200 ms for reads"), and when your design fans out (sharded search, a page aggregating several services, scatter-gather), mention tail amplification and one mitigation: "with 20 shards, the slowest shard dominates, so I'd add hedged requests after the p95 and return partial results after a deadline." It shows you understand how distributed systems behave, not just how they are drawn.

Best practices

  • Export latency as a histogram with bucket boundaries at your SLO thresholds β€” with a bucket edge at 300 ms, "p99 < 300 ms" is computed exactly instead of interpolated.
  • Tag latency per route, never one number per service β€” a fast detail page and a slow listing need separate percentiles. In BoxOffice: the name:detail and name:list tags in lab 2's load/load.js (box above).
  • Report p50, p99 and p99.9 together with the request count β€” a p99 over 200 requests is the second-slowest request; it needs thousands to mean anything.
  • Derive every timeout from a latency budget, and propagate the remaining deadline β€” a call gets at most its share of the caller's budget, so the caller never times out first and retries work still in progress.
  • Hedge only idempotent reads, after the p95, with a cap on extra load (a few percent) β€” hedging buys tail latency with load, and the load must stay bounded.

Common pitfalls

  • Reporting averages for latency β€” or SLOs on averages.
  • Averaging percentiles across servers or time windows instead of merging histograms.
  • Ignoring fan-out: the backend's p99 is not the user's p99 when one request makes many calls.
  • Hedging non-idempotent requests: duplicating a POST /orders duplicates the order.
  • Coordinated omission in load tests: the tail you do not measure is the tail you do not fix.

How it connects

flowchart LR
    DIST["long-tailed latency"] --> PCT["p50, p99, p99.9<br/>from histograms"]
    PCT --> SLO["latency SLOs<br/>β†’ Reliability"]
    PCT --> OBS["merge histograms,<br/>never average β†’ Observability"]
    FAN["fan-out N<br/>1 βˆ’ (1 βˆ’ q)α΄Ί"] --> AMP["the backend tail becomes<br/>the common case (large N)"]
    DIST --> SRC["sources: GC, cache misses,<br/>cold starts, slow dependencies"]
    AMP --> TAME["hedging, partial results,<br/>less fan-out, headroom"]
    SRC --> TAME
    TAME --> BUD["latency budgets<br/>and timeouts β†’ Reliability"]
    PCT --> CO["measure at the client,<br/>coordinated omission β†’ Performance"]

Palette components: search (20 shards: the fan-out tail) Β· app (a page that aggregates several services; hedging to another replica) Β· cache (misses are a tail source) Β· serverless (cold starts) Β· apigw (where the SLO is measured) Β· payment (the provider dominates the checkout budget) Β· metrics (histograms, never averaged percentiles). Scenarios: scale-out (with chaos), search-autocomplete. Continues in: Availability Math, Observability β€” SLOs, Reliability β€” timeouts, Observability β€” signals, Performance β€” load testing, Hands-on 2.

Key takeaways

  • Latency distributions have long tails; the mean hides them β€” use p50, p99, p99.9.
  • Percentiles come from histograms; aggregate by merging histograms, never by averaging percentiles.
  • Users and heavy customers make many requests, so the "1% tail" touches a large share of visits.
  • Under fan-out N, P(slow) = 1 βˆ’ (1 βˆ’ q)α΄Ί: with 20 calls the backend's p99 hits 18 % of user requests; from ~70 calls on, it is the user's median.
  • Fight the tail with hedging, partial results, less fan-out, headroom and async work; split end-to-end targets into latency budgets and matching timeouts.


  1. KLEPPMANN, M.; RICCOMINI, C. Designing Data-Intensive Applications, 2nd ed. O'Reilly. Chapter 2 (describing performance: response time percentiles and tail latency amplification). ↩

  2. DeCANDIA, G. et al. Dynamo: Amazon's Highly Available Key-value Store. SOSP, 2007 (service-level agreements expressed at the 99.9th percentile). ↩

  3. DEAN, J.; BARROSO, L. A. The Tail at Scale. Communications of the ACM, 56(2), 2013, pp. 74–80. ↩↩↩

  4. TENE, G. How NOT to Measure Latency. Talk, Strange Loop, 2015. ↩