Skip to main content

Performance Testing

Load Testing: What the Numbers Actually Tell You

Vuk KazimirovicJun 12, 20263 min read
Load Testing: What the Numbers Actually Tell You

A team runs a load test, sees a green result, and assumes the system is ready. Then real traffic spikes and something falls over that the test never touched.

Usually that means the test measured the wrong thing, not that testing failed.

Average response time hides the problem

A p50 of 200ms can sit next to a p99 of 4 seconds. Report only the average and that gap disappears completely, along with every user who's actually having a bad time.

  • Report p50, p95, and p99 together, not just the mean.
  • Track error rate alongside latency. A fast response that returns a 500 is not a success.
  • Watch what happens at the moment load ramps up, not just at steady state. Connection pools and autoscalers often struggle hardest right at the transition, not once load has settled.

A k6 script makes this explicit instead of something you check by eye afterward:

export const options = {
  stages: [
    { duration: "2m", target: 200 },
    { duration: "5m", target: 200 },
    { duration: "2m", target: 0 },
  ],
  thresholds: {
    http_req_duration: ["p(95)<400", "p(99)<1500"],
    http_req_failed: ["rate<0.01"],
  },
};

That thresholds block is what turns the test into a real pass/fail gate. Without it, "load test passed" just means the script finished running, not that any specific latency or error target was actually met.

Trusting a green load test result without checking what it actually measured?

An average response time hides the users having a genuinely bad time. We build thresholds around p95 and p99, not a single number that looks reassuring.

Test the shape of real traffic

A flat ramp to a fixed number of virtual users rarely matches how traffic actually arrives. Spikes, retries, and uneven request mixes change which component breaks first.

  1. Model the request mix from real usage data, not a guess.
  2. Include retry storms. A slow dependency plus client-side retries can multiply load on the exact service that's already struggling, which is usually the failure mode that a clean, evenly-spaced test never surfaces.
  3. Run a soak test, not just a spike test.

Soak tests catch a different class of failure than spike tests do. A memory leak that adds 2MB per thousand requests looks completely fine for the first twenty minutes of any test, spike or otherwise. It only becomes visible after hours of sustained load, which is exactly the condition a short test is built to skip past. The same is true for connection pool exhaustion: a pool that's slowly leaking connections passes a five-minute spike test and falls over three hours into a real launch day.

Know what actually broke

When a load test fails, the useful output is which resource hit its limit: database connections, thread pool size, a downstream API's rate limit. Not just "it got slow."

"It got slow" tells a team to look everywhere. "The connection pool hit its ceiling at 340 concurrent requests" tells them exactly what to fix, and whether the fix is a config change or a real architectural one. That specificity is the actual difference between a test result and something that gets acted on before it becomes an incident.

We build load test suites in k6, JMeter, or Gatling depending on the stack, and tie the thresholds to what actually matters for that system, not a generic throughput number picked because it sounded reasonable. That's the core of how we run performance testing engagements.