Percentiles: p50, p95 and p99
A percentile describes how a set of measurements is spread. The 95th percentile, p95, is the value at or below which 95% of the measurements fall: if a search API has a p95 response time of 600 ms, 95 of every 100 searches took 600 ms or less and 5 took longer. p50 is the median, the typical request; p95 and p99 describe the slow end, which testers call the tail.
The lesson on performance metrics introduced response time, throughput and error rate. This one is about summarising response times: thousands of measurements become a few numbers you can compare with a target, and choosing percentiles over the average decides whether slow requests appear in your report at all.
Why it matters
Users do not experience an average; they remember their slow requests. A few very slow requests can hide behind a healthy-looking average, so "average 220 ms" can describe a release in which one request in ten takes over a second.
Percentiles make performance testable. "The API should be fast" cannot fail; "p95 below 500 ms at 200 requests per second" can. They also let you compare two builds fairly and describe a regression in terms a developer can act on.
What a percentile is and how to calculate it
Sort every response time measured for one operation from fastest to slowest. The pth percentile is the value at or below which p percent of them lie. By hand, use the nearest-rank method: rank = ceil(p / 100 × n), that is p percent of the count rounded up, and read the value at that rank.
With 20 measurements, p50 is the value at rank 10, p95 at rank 19 and p99 at rank 20, which is simply the slowest request. A percentile is only as precise as the data behind it: p99 rests on the slowest 1%, just 2 requests out of 200, so it needs hundreds or thousands of samples.
- Some tools use the nearest rank, others interpolate between neighbouring values, so with few samples two tools can report different numbers for the same run. With thousands of samples the difference is small.
- Labels differ: k6 writes
p(95), and the Aggregate Report in JMeter has a95% Linecolumn. Check which percentiles a summary shows; in k6 you choose them with thesummaryTrendStatsoption. - Compute percentiles per operation: a 5 ms health check and a 400 ms search in one list give a p95 that describes neither.
What p50, p95 and p99 each tell you
Each percentile answers a different question, so reports show several side by side. And "only 1%" is more than it sounds: at 50 requests per second it means 30 slow requests a minute, and if a page makes 10 independent calls, each with a 1% chance of being slower than its p99, the chance that at least one is slow is 1 - 0.99^10, about 10%.
- p50, the median: half the requests were faster, half slower. It shows the typical experience and moves when a change slows everything down, such as a query that lost its index.
- p95: one request in 20 was slower. It is a common choice for test targets and SLOs because it covers almost every request and is less sensitive to a single outlier than p99.
- p99: one request in 100 was slower. This is the tail, where garbage-collection pauses, lock contention, cold caches, retries and slow dependencies show up. It needs many samples and often rises before the median as load grows.
- The maximum is a single request: check it for timeouts, but it is too noisy to be a target. High-traffic services also track p99.9.
Why averages mislead
Response times are skewed: most requests cluster near the fast end, and a long tail of slow ones stretches towards high values. The mean adds every value and divides by the count, so a few slow requests pull it up, yet it says nothing about how many users waited.
Two services each handle 100 requests with an average of 200 ms. Service A answers 20 requests each in 180, 190, 200, 210 and 220 ms. Service B answers 88 requests in 100 ms, 7 in 600 ms, 4 in 1,250 ms and one in 2,000 ms. A has p50 200 ms, p95 220 ms and p99 220 ms; B has p50 100 ms, p95 600 ms and p99 1,250 ms. B is faster for most users and much slower for the unlucky ones, and only A meets a p95 target of 500 ms.
Service A: 100 requests, all between 180 and 220 ms
- ≤ 500 msp95 target met
- > 500 msp95 target missed
- 1200 ms p50 = averageHalf the requests took 200 ms or less, and the mean is also 200 ms: without a tail, the two agree.
- 2220 ms p95 = p99Within the 500 ms target: even the slowest requests took only 220 ms.Pass
Service B: 88 requests at 100 ms, 12 between 600 and 2000 ms
- ≤ 500 msp95 target met
- > 500 msp95 target missed
- 1100 ms p50 (median)The typical request is twice as fast as in A.
- 2200 ms AverageExactly as in A: the mean, 200 ms, cannot tell the two services apart.
- 3600 ms
p95Over the 500 ms target: 12 of the 100 requests took 600 ms or more.Fail - 41250 ms
p99The tail: 5 requests took 1250 ms or more.
Percentiles by the nearest-rank method. B's slowest request, 2000 ms, lies beyond the end of the axis.
- An average can describe a request that never happened: if 80% of requests hit a cache in 20 ms and 20% miss and take 800 ms, the mean is 176 ms, a time no request took.
- You cannot average percentiles. The p95 of two servers, or of a day of one-minute windows, is not the mean of their p95 values; compute it from the raw measurements or a merged histogram (counts of requests per latency range).
- An error returned in 5 ms can pull the average and the percentiles down and make a failing system look fast. Report the latency of successful requests and the error rate side by side.
Worked example: 20 search requests
After a release, a tester sends 20 sequential requests to the search API on staging, with no other load, to check the team's target: p95 at or below 500 ms. The block below holds the raw times, the sorted list and the calculation.
- Mean: 4,400 / 20 = 220 ms. It looks comfortable, yet 17 of the 20 requests were faster than the average and two were far slower.
- p50: rank 10 → 125 ms. The typical search is quick.
- p95: rank 19 → 600 ms. The target is not met, and the second-slowest request decides it.
- p99: rank 20 → 1,400 ms, just the maximum. Twenty samples cannot support a p99, so do not report one.
- The report gives p50, p95 and the maximum with the sample size and load, states that the target is not met, names the slow requests (the 4th and the 10th) for investigation, and asks for a rerun with several hundred requests under the agreed load before any decision.
Search API, staging, 20 sequential requests, response time in ms
Target: p95 <= 500 ms
As measured: 128 113 96 600 122 136 104 150 117 1400
110 131 194 101 125 240 142 108 163 120
Sorted:
rank 1 2 3 4 5 6 7 8 9 10
ms 96 101 104 108 110 113 117 120 122 125
rank 11 12 13 14 15 16 17 18 19 20
ms 128 131 136 142 150 163 194 240 600 1400
Mean = 4400 / 20 = 220 ms
p50 = rank ceil(0.50 x 20) = 10 -> 125 ms
p95 = rank ceil(0.95 x 20) = 19 -> 600 ms (target 500 ms: not met)
p99 = rank ceil(0.99 x 20) = 20 -> 1400 ms (= the maximum)Writing latency targets and SLOs with percentiles
In the Google SRE book, a service level indicator (SLI) is a carefully defined measurement of the service, such as request latency, and a service level objective (SLO) is the target value or range for it. A service level agreement (SLA) is a contract with consequences when the objective is missed. Latency objectives use percentiles because they must hold for nearly all users, not for an average one.
A complete target names five things: which requests (the operation; successful only or all), the percentile, the threshold, the window or test duration, and the conditions such as load. "95% of requests complete within 500 ms" and p95 ≤ 500 ms are the same condition. In k6 a criterion becomes thresholds: when one is missed, the run fails with a non-zero exit code, so a CI pipeline can stop on it. The script at the end of this section turns the load-test criterion below into k6: its scenario starts 200 requests per second whatever the response time, drawing on a pool of 100 to 400 virtual users (preAllocatedVUs, maxVUs) to send them; the next lesson, on concurrent users and workload models, explains this open model and how to size the pool. The {expected_response:true} filter limits the latency thresholds to successful responses.
- Production SLO: "99% of successful
GET /products/{id}requests complete within 300 ms, measured over a rolling 28 days." - Two levels for checkout: "
p95 ≤ 800 msandp99 ≤ 2 sforPOST /checkout", bounding both the typical slow request and the tail. - A load-test criterion derived from an SLO: "For
GET /searchat 200 requests per second for 30 minutes:p95 ≤ 500 msandp99 ≤ 1200 msfor successful requests, error rate below 0.5%." The numbers in these examples are illustrations; real ones come from user needs and the product team, not from a standard.
import http from 'k6/http';
export const options = {
scenarios: {
search: {
executor: 'constant-arrival-rate', // 200 iterations a second, one request each
rate: 200,
timeUnit: '1s',
duration: '30m',
preAllocatedVUs: 100,
maxVUs: 400,
},
},
thresholds: {
'http_req_duration{expected_response:true}': ['p(95)<=500', 'p(99)<=1200'],
http_req_failed: ['rate<0.005'],
},
};
export default function () {
http.get('https://staging.example.com/search?q=lamp');
}Common mistakes
- Reporting only the average response time, or headlining it above the percentiles.
- Quoting p99 from fewer than 100 samples, where the nearest-rank p99 is just the slowest request.
- Averaging percentiles across servers, endpoints or time windows instead of computing them from raw data.
- Mixing different operations, or successful and failed requests, in one set of response times.
- Writing a target without its conditions: a p95 measured with one user says nothing about p95 at peak load.
- Comparing results from two tools or runs without checking that they used the same percentile method, requests and load.
The QA perspective
During requirements review, ask for latency targets as percentiles and complete them with the product owner and the developers or SREs who run the service: which operations, which percentile, measured where and over what window, under what load, and whether failed requests count. "Search must be fast" becomes testable only after that conversation.
In every report, show p50, p95 and, when the sample is large enough, p99 per operation with the sample size, error rate, load, tool and percentile method. When a percentile misses its target, give developers the slow requests themselves, with timestamps and parameters, and compare each build with the previous one under the same conditions so a regression in the tail is caught before users feel it.
Practice exercise
Check a release against its percentile target
A colleague tested the order-history API on staging after release 2.8 and wrote: "Average 190 ms, well within the 400 ms target. Pass." Using the material below and pen and paper or a spreadsheet, work out the real verdict.
Afterwards, on an endpoint you own or are allowed to test, compare the average, median and p95 that the Response Time Analyzer reports.
Order history API, staging, release 2.8, 21 requests sent, no concurrent load
Target: p95 <= 400 ms and p99 <= 1000 ms, successful requests only
Successful (ms): 118 95 136 87 470 110 121 104 158 92
131 960 112 99 144 127 390 107 124 115
Failed: 1 request, HTTP 500 after 9 ms- Sort the 20 successful response times and calculate the mean.
- Find p50, p95 and p99 with the nearest-rank method.
- Compare each result with the target, decide pass or fail, and note what 20 samples cannot tell you.
- Decide how the failed request counts, then write the result in two sentences.
Show the model answer
Sorted: 87, 92, 95, 99, 104, 107, 110, 112, 115, 118, 121, 124, 127, 131, 136, 144, 158, 390, 470, 960. The sum is 3,800 ms, so the mean is 190 ms: the colleague's arithmetic is right.
p50 = rank 10 = 118 ms; p95 = rank 19 = 470 ms; p99 = rank 20 = 960 ms, the maximum.
Verdict: fail. p95 is 470 ms against 400 ms, and 2 of the 20 requests took longer than 400 ms. p99 is met on paper, but with fewer than 100 samples it is only the slowest request and proves nothing.
The HTTP 500 stays out of the latency figures ("successful requests only") but must be reported: 1 of 21 requests failed, about 4.8%. Counted in, its 9 ms would have lowered the average to about 181 ms, flattering a failing release.
Result: "Release 2.8 misses its latency target: p95 470 ms against 400 ms (p50 118 ms, maximum 960 ms; 20 successful sequential requests, no concurrent load), and 1 of 21 requests failed with HTTP 500. Rerun with several hundred requests under the agreed load and investigate requests 5 and 12 and the error."
Frequently asked questions
Is p50 the same as the average?
No. p50 is the median: the middle value once the measurements are sorted. The average (mean) is the sum divided by the count. They are equal only for a symmetric distribution; response times usually have a slow tail that pulls the average above the median.
Should my target be p95 or p99?
Usually both, for different reasons. p95 bounds what nearly every request experiences and needs fewer samples than p99 to be stable; p99 bounds the tail, which matters when users make many requests or when a service sits behind others. The maximum is a single request and too noisy to be a target, but always look at it for timeouts.
How many requests do I need for a reliable p99?
Far more than 100. With the nearest rank, p99 of fewer than 100 measurements is simply the slowest request, and at 1,000 measurements it still rests on the slowest 10. Collect thousands of samples per operation, or measure over a longer window, before you treat a p99 as a stable number.
What is the difference between an SLO and a performance test target?
An SLO is an objective for the running service, measured continuously over a window such as 28 days. A performance test target applies to one test run under defined conditions. Testers usually derive the test target from the SLO, so that a release that passes the test is likely to keep the SLO in production.
References
- Site Reliability Engineering: How Google Runs Production SystemsService level objectives, error budgets, monitoring the "four golden signals" and handling overload — the operational side of resilience.
- Monitoring Distributed Systems — Site Reliability Engineering (Rob Ewaschuk)Google's SRE guidance on monitoring: black-box versus white-box monitoring and the four golden signals — latency, traffic, errors and saturation.
- Grafana k6 documentationOfficial documentation for Grafana k6, an open-source and extensible load and performance testing tool.
- Apache JMeter User's ManualHow to build, parameterise and run load tests with JMeter, and how to read its listeners and reports.
Practise with Mutqan tools
Measure an endpoint's response time over several samples: min, max, average, median and p95.
Run a small, safe benchmark against a public endpoint (bounded requests and concurrency) and get throughput and latency percentiles.
Compare response times and sizes of two endpoints (e.g. v1 vs v2, staging vs production) side by side.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.