Four load generators, one stalling server, and a p99 that differs by 302x
The same server, stalling for a full second every ten. One tool reported a 3 millisecond p99. Another, measuring the same stalls, reported 907 milliseconds.
Here is a server with an obvious problem. Every ten seconds it takes a global lock and holds it for a full second, so everything in flight blocks adn everything arriving queues behind it. Any user hitting it during that second waits up to a second. Now measure it. One popular tool will tell you the p99 is three milliseconds and the server is healthy, and it will not be lying, because it genuinely never observed a slow request often enough for the percentile to notice. The reason is the thing worth understanding, and it is not about the server at all.
What I compared on
- the p99 each tool reports for the identical server and the identical stalls
- whether the tool keeps sending during a stall or waits it out
- whether the tool tells you it failed to keep up
01 vegeta
Open loop generator driven by a target rate, which keeps sending at that rate regardless of what the server is doing.
02 k6
Scriptable generator which, with the constant-arrival-rate executor, starts iterations on a schedule rather than on completion.
03 autocannon
Node based closed loop generator, the usual choice for benchmarking a JavaScript service from the same ecosystem.
04 ApacheBench
The ab binary already on most machines, holding a fixed concurrency and racing as fast as the server will answer.
Side by side
| Model | p50 | p99 | Max | |
|---|---|---|---|---|
| ApacheBench | closed loop | 1 ms | 3 ms | 2,296 ms |
| autocannon | closed loop | 41 ms | 43 ms | 1,036 ms |
| k6 constant arrival | open loop | 0.23 ms | 906 ms | 1,000 ms |
| vegeta | open loop | 41 ms | 907 ms | 1,043 ms |
Verdict
Use k6 with the constant-arrival-rate executor, and read the dropped iterations line as carefully as you read the percentiles. It was the only tool that both measured the stall properly and told me it had failed to keep up while doing so, which together are the two halves of the answer. The general rule this test makes concrete: if your generator waits for a response before sending the next request, it stops sampling precisely when the server is worst, so its tail latency describes the server's good moments at high resolution and its bad ones barely at all. Check which model your tool uses before you trust a p99 from it.