Why the Dashboard Says 180 ms While Five Users Waited Six Seconds
qa-performance-concepts-baseline-percentiles -->
Your checkout API has a monitoring panel that proudly reports average response time: 180 ms, yet support keeps getting tickets saying checkout "hung for several seconds." Both statements are true, and the gap between them is the single most common performance mistake in interviews and in production: the average is the wrong summary for latency.
Latency is a distribution, and a distribution is summarized with percentiles. Take every request in the window, sort them by duration, and read the value at each position:
1,000 requests sorted ascending
950 × 120 ms → p50 = 120 ms, p95 = 120 ms
45 × 800 ms → p99 = 800 ms
5 × 6,000 ms → worst case, gateway timeout
average = (950×120 + 45×800 + 5×6,000) / 1,000 = 180 ms
The average is 180 ms and five real people waited six seconds. Reporting only the mean hides exactly the users who file tickets. The percentile set you should always report together:
| Metric | Question it answers | The trap if you skip it |
|---|---|---|
| p50 | What does a typical request experience? | Flatters you when a minority is slow |
| p95 | What does an unhappy-but-common user see? | Misses the tail your SLA is written against |
| p99 | What does one request in a hundred suffer? | Regressions appear here first, weeks before the mean moves |
| max | What is the absolute worst observed? | Noisy — use it for hunting, not for thresholds |
Interviewers ask "how would you measure this API?" to see whether you reach for p95/p99 instead of the mean. Say percentiles first, then throughput, then error rate.
Throughput is the second pillar: requests completed per second (rps) or transactions per second (tps) over the window. It only means something next to latency, and Little's Law connects them:
concurrent requests in flight = throughput × latency
40 rps × 0.5 s = 20 requests in flight
That equation is a sanity check you can run in your head during a load test. If throughput stays at 40 rps but p95 climbs from 500 ms to 2 s, concurrency has quadrupled to 80 — your thread pool, database pool, or upstream is queueing, and the queue is now the bottleneck, not the CPU. It also converts user counts into a load model: 200 users who each issue a request every 5 seconds is 40 rps, not 200 concurrent.
Error rate is the third pillar and the one teams forget under load. Count HTTP 5xx, gateway timeouts, connection resets, and — critically — correctness failures: a 200 response that returns a stale price is a failed request. Timed-out requests must be counted as errors at the timeout threshold (say 6 s), never quietly dropped as "slow successes," or your error rate reads 0 % while users see failures. That is why performance runs need the same response assertions functional tests use, a point Test Reporting and Dashboards already made for functional runs: a green run with wrong answers is not green.
Hold the three metrics in one sentence when you report: p95 latency, throughput at that latency, and error rate — for example "240 rps at 310 ms p95 with 0.02 % errors." Any one of them alone is a number without meaning, and the follow-up to this topic, Designing JMeter Test Plans, is where you start producing those triples on demand.
Load, Stress, Spike, or Soak: Choose the Test by the Question You Are Asking
qa-performance-concepts-baseline-test-types -->
Teams routinely run one long test at one load level and call it "performance testing." It answers exactly one question, usually the least interesting one. Each test type is defined by the shape of the load curve and the question it answers — pick the question first, then the shape.
| Test | Load shape | The question | Passing looks like | Typical length |
|---|---|---|---|---|
| Load | Ramp to expected peak, hold | Does the system meet its SLO at normal peak? | p95 under target, error rate near zero, flat latency through the hold | 10–30 min |
| Stress | Ramp past peak until it breaks | Where is the ceiling, and does it fail gracefully? | Degradation is orderly: 429s or queued, then recovery after load drops | 20–40 min |
| Spike | 0 → 10× in seconds, hold, drop | Do buffers, autoscaling, and pools react fast enough? | No dropped connections; latency recovers within seconds of the drop | 5–10 min |
| Soak | ~70 % of peak, constant, hours | Does steady use leak anything? | Flat memory, connections, disk, and latency for the whole run | 6–8 hours |
Load is the release gate: it validates the number in your SLA. The classic mistake is testing at exactly 100 % of expected peak with zero headroom — real traffic has bursts, so test at peak plus 20–30 %.
Stress deliberately walks past the ceiling. You are watching the shape of collapse. A healthy system at 150 % load returns 429 with Retry-After and holds p95 at a degraded but finite value. An unhealthy one shows latency compounding without bound (the queue grows: remember Little's Law — latency climbs, so concurrency climbs, so the queue grows) and connections timing out. The follow-up question interviewers add: does the system recover when load returns to normal, or is it wedged? Run the stress test long enough to include that recovery ramp.
Spike is a different physics problem. Autoscalers poll every 30–60 s, TCP backlog is finite, and a database pool of 20 connections does not grow on demand. A 10× burst in 2 seconds tests whether the front door absorbs or drops: expect connection errors if the buffer is too small, or a latency cliff if every request queues behind 20 pool slots.
Soak finds the bugs no short test can. Real numbers from runs that mattered: a JVM heap growing 40 MB/hour until java.lang.OutOfMemoryError at hour 6; a connection pool leaking one socket per minute so hour 5 sees HikariPool-1 - Connection is not available, request timed out after 30000ms; a log volume filling at 1 GB/hour. Every one of those passes a 20-minute load test. If you only have budget for two tests, run load on every release and soak nightly — a soak failure at 03:00 is far cheaper than one at 09:00 in front of customers.
Notice what the table shares: every row needs a stable load generator and repeatable numbers, which is what the next topic builds — Designing JMeter Test Plans turns these four shapes into reusable thread-group configurations.
Capturing a Baseline on Staging That Survives the Next Release
qa-performance-concepts-baseline-establishing -->
"Is checkout slower than last sprint?" is unanswerable unless someone wrote down what slower meant before the change. A baseline is a stored, trusted set of measurements — p50/p95/p99, throughput, error rate — taken on a known build under known conditions, against which every future run is compared. Without one you are comparing vibes.
The procedure that produces a trustworthy number:
- Fix the environment. Same instance types, same DB size, same data volume. Note the commit SHA and the build number alongside the results — a baseline with no version attached is a rumor.
- Fix the data. Seed an identical dataset (say 50,000 orders, 200,000 users) and reset it between runs. Query plans change when tables grow; a baseline taken on an empty database measures an application nobody runs.
- Warm up, then repeat. Discard the first run (JIT compilation, connection-pool fill, cold caches), then run at least 3 times and record the spread.
- Store the artifact, not just a number — the JTL/CSV and the generated report belong next to the trend data you already publish from Test Reporting and Dashboards.
A small baseline table for one service might look like this:
| Endpoint | Baseline p95 | Alert threshold | Throughput | Error budget |
|---|---|---|---|---|
POST /checkout |
310 ms | 380 ms (+20 %) | 120 rps | 0.1 % |
GET /catalog |
90 ms | 110 ms | 300 rps | 0.05 % |
POST /login |
220 ms | 275 ms | 40 rps | 0.1 % |
Three rules keep those numbers honest:
- Account for noise. Staging is shared. Expect roughly ±5–10 % run-to-run variation on p95; set your alert threshold above it (a common choice is baseline × 1.2), or you will spend weeks chasing the background job a colleague triggered at 14:00. If a run breaches the threshold, re-run before filing a bug — two consecutive breaches is a regression, one is weather.
- Never compare across environments. A staging box that is half the production size will produce p95 numbers 2–4× worse, and the ratio does not transfer. Either baseline production passively (real traffic percentiles from your APM) or keep staging sized proportionally and say so in the report.
- Compare like-shaped runs. Same virtual-user count, same ramp, same payload size, same think time. A baseline taken at 50 users compared against a run at 200 users measures load, not regression.
The comparison itself can be a five-line script rather than a person staring at two charts:
k6 run --summary-export new.json checkout.js # candidate build
node -e '
const base = require("./baseline/summary.json").metrics.http_req_duration.values;
const cur = require("./new.json").metrics.http_req_duration.values;
const ratio = cur["p(95)"] / base["p(95)"];
console.log(`p95 ${base["p(95)"]} -> ${cur["p(95)"]} (${(ratio*100).toFixed(0)}%)`);
process.exit(ratio > 1.2 ? 1 : 0);'
That exit code is the whole point: the baseline stops being a document people consult and becomes the gate the pipeline enforces. Store the baseline artifacts per release — the summary JSON, the JTL, the commit SHA, the seed-data version — so that when someone asks "when did checkout get slow?", the answer is a bisect over stored baselines rather than archaeology.
One more placement decision matters: how much of the baseline comes from production. Staging baselines answer "did this build regress under this load model," while passive production monitoring answers "is the real p95 within the SLO the business promised." You want both — the staging gate catches the regression before release, and the production percentile confirms the promise after it. The next topic builds the first of those two instruments, turning this procedure into a repeatable JMeter plan: Designing JMeter Test Plans.
Quiz
1. A dashboard reports average latency of 180 ms, but 1 % of requests take 6 seconds. Which reporting change exposes the problem?
2. A load test holds 40 rps; p95 climbs from 500 ms to 2 s while throughput stays flat. What does that combination indicate?
3. A JVM heap grows about 40 MB per hour and the service dies around hour six. Which test finds this?
4. A new build shows p95 of 350 ms against a 310 ms baseline on shared staging where run-to-run variation is about ±10 %. What is the correct call?
Flashcards
Question
Why do p95 and p99 matter more than average latency?
Click to reveal answer
Answer
Latency is a distribution; the mean dilutes the slow tail that produces user complaints and timeouts. p50 describes the typical request, p95 the unhappy-but-common one, p99 the one-in-a-hundred regression detector — always report the percentile set with throughput and error rate.
Question
State Little's Law and one load-testing use for it.
Click to reveal answer
Answer
Concurrent requests in flight = throughput x latency (40 rps x 0.5 s = 20 in flight). Use it to convert a user count into a rps model (200 users, one request per 5 s = 40 rps) and to diagnose runs: flat throughput with rising latency means concurrency grew, so something is queueing.
Question
Distinguish load, stress, spike, and soak tests by the question each answers.
Click to reveal answer
Answer
Load: does the system meet its SLO at peak (+20-30 % headroom)? Stress: where is the ceiling and does it fail gracefully (429s, recovery)? Spike: do buffers/autoscaling/pools absorb a sudden 10x burst? Soak: does 6-8 h at ~70 % of peak leak memory, connections, or disk?
Question
What makes a performance baseline trustworthy?
Click to reveal answer
Answer
Same environment and seeded data, pinned commit, warm-up run discarded, at least 3 runs recorded with the spread, artifacts (JTL/report) stored with the numbers, and alert thresholds set above run-to-run noise (about 5-10 %, commonly baseline x 1.2). Never compare across differently sized environments.
Question
How should timeouts and 200-OK error responses be counted?
Click to reveal answer
Answer
Timeouts count as errors at the timeout threshold (e.g. 6 s), never as slow successes; 200 responses carrying error payloads need business-level assertions to be caught. Otherwise the error rate reads 0 % while users experience failures.
Revision Notes
Key Takeaways
- 1. The average is the wrong summary for latency; p95 and p99 are computed from the sorted distribution and expose the tail users complain about
- 2. Little's Law converts between users, rps, and latency — use it to build the load model and to recognize queueing when throughput stays flat
- 3. Each test type answers one question: load = SLO at peak, stress = ceiling and recovery, spike = burst handling, soak = slow leaks
- 4. A baseline is only comparable to an identical-shaped run on the same environment, data, and load level
- 5. Timeouts are errors at the timeout threshold, and 200 responses with error payloads need assertions — or error rate lies
Interview Tips
- • "How do you measure an API's performance?" — answer p95/p99 latency, throughput at that latency, and error rate in one sentence, then mention baseline comparison; never lead with the average
- • "50 users reported slowness but the dashboard was green — why?" — the dashboard showed the mean; the tail was in p99, and percentiles per endpoint would have localized it
- • "Which test would you run before a Black Friday launch?" — load at peak+30 %, plus a soak for leaks and a spike for burst absorption; explain what each fails to catch
- • "How do you avoid false regression alarms?" — account for run-to-run noise, set thresholds above it, require consecutive breaches, re-run before filing
Cheat Sheet
Performance Concepts and Baselines
Report every run as one sentence: "240 rps at 310 ms p95, 0.02 % errors" — any single metric alone is meaningless.
| Metric | Question | Trap |
|---|---|---|
| p50 | typical request | flatters a slow minority |
| p95 / p99 | unhappy / one-in-a-hundred user | regressions appear here first |
| throughput (rps) | how much work per second | meaningless without latency |
| error rate | 5xx + timeouts + wrong answers | 200-with-error-body hides in it |
Little's Law: in-flight = throughput x latency. Flat rps + doubled latency = queueing (pool/lock/downstream), not CPU.
Test types: load (SLO at peak+20-30 %) · stress (ceiling + graceful failure + recovery) · spike (burst absorption) · soak (6-8 h at ~70 % → leaks).
Baseline recipe: pin env + commit + seeded data → discard warm-up run → 3+ runs, record spread → store JTL/report → alert at baseline x 1.2 → re-run before filing on a single breach. Never compare across environments or load levels.