p50, p95, p99: Percentiles for People Who Skipped Stats
Here are ten response times from an API endpoint, in milliseconds:
120, 130, 140, 150, 160, 170, 180, 200, 240, 4000
The average is 549 ms. Nine of the ten users got a response in under a quarter second, one user waited four seconds, and the average — 549 ms — describes none of them. It’s slower than anything nine users experienced and seven times faster than what the tenth got. This is the whole case for percentiles in one line of numbers, and it’s why the p95 latency meaning question matters more than it sounds: percentiles are how you talk about latency without letting one number lie to you.
p95 latency meaning: a position in a sorted list
Sort your response times from fastest to slowest. A percentile is just a position in that sorted line: pXX is the value that XX% of requests were faster than (or equal to).
For our ten numbers:
- p50 = 165 ms. Half of requests finished by here. This is the median — the “typical” request.
- p90 = 240 ms. Nine out of ten finished by here.
- p95 ≈ 4000 ms. With only ten samples, the 95th percentile lands on the worst request. Which is itself a lesson — hold that thought.
So when someone says “p95 latency is 800 ms,” the meaning is exactly this: the slowest 5% of requests took longer than 800 ms; everyone else was faster. No model, no bell curve, no assumptions. It’s a fact about a sorted list.
That’s all the math there is. Everything else in this article is about which position in the line to look at, and why.
Why averages lie about user experience
Latency distributions are lopsided in a specific way: there’s a floor (a request can’t take less than ~0 ms) but no ceiling (a request can hang for 30 seconds). So the distribution grows a long tail to the right, and every value in that tail drags the average up while leaving the median untouched. One 4-second outlier moved our average from 165 to 549 — a 3x distortion from a single request.
The deeper problem is that the tail isn’t noise. The tail is your users — usually your heaviest ones. A user’s session might touch fifty requests; the odds that all fifty dodge the slowest 5% are 0.95⁵⁰ ≈ 8%. Put differently: at a p95 you’d consider rare, roughly nine out of ten active users hit tail latency at least once per session. The average says everything is fine. The average is not in the room where your biggest customer just watched a spinner for four seconds.
What each percentile answers
Each percentile is the answer to a different question. Pick by question, not by convention:
| Percentile | The question it answers | Use it for |
|---|---|---|
| p50 | “What does a typical request feel like?” | Product decisions, tracking trends |
| p75 | “Is the experience good for most users?” | Web vitals (Google’s Core Web Vitals report at p75) |
| p95 | “How bad is a normal-bad day?” | SLOs, alerting — the workhorse |
| p99 | “How bad is the worst case we routinely produce?” | Capacity planning, tail-hunting on high-traffic services |
| p100 (max) | “What’s the single worst thing that happened?” | Curiosity. Never alert on it. |
Opinionated summary: track p50 and p75 to know what you built; alert on p95 to know when it breaks. p99 is a diagnostic tool for services doing serious volume, not a default.
Why p99 alerts page you for ghosts
Here’s the trap teams fall into: “we care about the worst experiences, so we’ll alert on p99.” At low traffic, p99 isn’t a statistic — it’s an anecdote.
If your endpoint gets 200 requests an hour, p99 is determined by the two slowest requests of that hour. Two. One user on hotel wifi, one cold cache, one garbage-collection pause, and your p99 doubles. The alert fires, you investigate, and the answer is “a guy in a hotel.” Do that four nights in a row and the alert is dead — you’ll ignore it the night it’s real. (Our ten-number example already showed this in miniature: its p95 was the single worst request.)
Rule of thumb: a percentile is only as trustworthy as the number of samples above it. Alert on tail percentiles only where the tail contains dozens of requests per evaluation window. At 200 requests/hour, that means p95 at most — and honestly, an alert on “p50 doubled” will catch real regressions faster than any tail metric at that volume.
Computing percentiles from RUM data
RUM — real user monitoring — means these numbers come from actual browsers, not your load tester, so the distribution is uglier and the tails are fatter. The computation stays simple. Nearest-rank method: sort your N values ascending, and pXX is the value at position ⌈N × XX/100⌉ (round up, 1-indexed). For 1,000 page loads, p95 is the 950th value in sorted order. You can do it in one line:
const p = (values, q) => {
const s = [...values].sort((a, b) => a - b);
return s[Math.ceil(s.length * q) - 1];
};
p(loadTimes, 0.95); // p95
(Implementations vary in how they interpolate between ranks — our p50 of 165 above averaged the two middle values — which is why two tools can report slightly different percentiles for identical data. Nobody’s wrong; the definitions differ at the edges.)
Two field warnings, both of which have burned real dashboards:
You can’t average percentiles. The p95 of server A and the p95 of server B do not combine into a fleet-wide p95 — not by averaging, not by any formula. A percentile is a position in one sorted list; two lists have to be merged before the position means anything. Aggregate the raw values (or histogram buckets), then take the percentile. Any dashboard showing “avg(p95) across hosts” is displaying a number with no meaning.
Segment before you panic. Global RUM percentiles blend fiber-connected desktops with 3G phones in a moving car. A p95 regression might mean your code got slower — or that a marketing campaign in a low-bandwidth region worked. Split by connection type, device class, and geography before concluding anything.
One last connection: if you sample your RUM data to control volume, percentiles still work — a fair 10% sample gives nearly identical p50/p95 to the full dataset, though deep-tail estimates get noisy for the same too-few-samples reason as the p99 trap above. How to sample without losing the signal is its own topic: sampling, explained.
See the bug the way your user did
LogReplay captures session replays, console output, network requests, and errors in one timeline — so you stop guessing what happened before the ticket arrived.
Try LogReplay free