How to read load test results
Read the second table for throughput, the first for latency, and the line above the summary for failures. Here npx autocannon@8 -c 20 -d 10 http://127.0.0.1:9670/ok reported 1 284 Req/Sec at 15.11 ms mean. The same server failing one request in three reported 1 925 and 9.96 ms.
Why check this
A load test report reaches you more often than you run one: in a pull request, a release note, or a vendor summary. Read it before sign-off, and whenever a report claims an improvement.
The failure this catches is a release approved on a report that measured a broken service. A handler returning 503 in under a millisecond raises throughput and lowers mean latency at once. Every headline number improves while a third of the callers get nothing.
Generator and target share the eight cores of this machine, so these figures describe the pair.
Prerequisites
- Node 22 or later, and
npx autocannon@8, which fetches autocannon on first use. - A free port. Port 9670 is used below.
- The target, saved as
results-server.mjs./okanswers after a timer./failingreturns 503 for every third request and does no work for it.
// results-server.mjs
import { createServer } from 'node:http';
const counts = { ok: 0, failed: 0 };
let seen = 0;
createServer((req, res) => {
if (req.url === '/count') {
res.writeHead(200, { 'content-type': 'application/json' });
return void res.end(JSON.stringify(counts));
}
if (req.url === '/reset') {
counts.ok = 0; counts.failed = 0; seen = 0;
res.writeHead(200, { 'content-type': 'application/json' });
return void res.end('{"reset":true}');
}
seen += 1;
if (req.url === '/failing' && seen % 3 === 0) {
counts.failed += 1;
res.writeHead(503, { 'content-type': 'application/json' });
return void res.end('{"error":"busy"}');
}
counts.ok += 1;
setTimeout(() => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end('{"ok":true}');
}, 5);
}).listen(9670, '127.0.0.1', () => console.log('listening on 9670'));
- The 5 ms timer lands near 15 ms here, because this Windows build has a coarser tick. That is why the healthy latency below reads 15 ms.
- Point load at a host you own. Loopback has no network in it, so every latency figure is service time.
Steps
- Step 1.
Confirm the port has no listener.
netstat -ano | grep ":9670 " | grep -c LISTENING0Any other number means another process owns the port.
- Step 2.
Start the target.
node results-server.mjs > server.log 2>&1 & sleep 2; netstat -ano | grep ":9670 " | grep LISTENINGTCP 127.0.0.1:9670 0.0.0.0:0 LISTENING 38676The last column is the process id for step 6.
- Step 3.
Run the load against the healthy route.
npx autocannon@8 -c 20 -d 10 http://127.0.0.1:9670/okRunning 10s test @ http://127.0.0.1:9670/ok 20 connections ┌─────────┬──────┬───────┬───────┬───────┬──────────┬─────────┬───────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼──────┼───────┼───────┼───────┼──────────┼─────────┼───────┤ │ Latency │ 8 ms │ 15 ms │ 20 ms │ 24 ms │ 15.11 ms │ 2.24 ms │ 32 ms │ └─────────┴──────┴───────┴───────┴───────┴──────────┴─────────┴───────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 1 240 │ 1 240 │ 1 280 │ 1 340 │ 1 284 │ 26,54 │ 1 240 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 228 kB │ 228 kB │ 236 kB │ 247 kB │ 236 kB │ 4.88 kB │ 228 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ Req/Bytes counts sampled once per second. # of samples: 10 13k requests in 10.08s, 2.36 MB readThe two tables count different things. The latency row is a distribution over 13 000 requests, so its
50%cell, 15 ms, is the median request. TheReq/Secrow is a distribution over 10 numbers, one per second, which# of samples: 10states. ItsAvg, 1 284, is the throughput figure.The last column differs on purpose. Latency ends at
Maxand throughput atMin, because the worst second is the smallest one. Neither table has a column for failures, and the footer counts completed responses of any status. - Step 4.
Run the same load against the route that fails one request in three.
curl -s http://127.0.0.1:9670/reset > /dev/null && npx autocannon@8 -c 20 -d 10 http://127.0.0.1:9670/failingRunning 10s test @ http://127.0.0.1:9670/failing 20 connections ┌─────────┬──────┬───────┬───────┬───────┬─────────┬─────────┬───────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼──────┼───────┼───────┼───────┼─────────┼─────────┼───────┤ │ Latency │ 0 ms │ 15 ms │ 17 ms │ 24 ms │ 9.96 ms │ 7.32 ms │ 33 ms │ └─────────┴──────┴───────┴───────┴───────┴─────────┴─────────┴───────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 1 839 │ 1 839 │ 1 910 │ 2 040 │ 1 925 │ 51,94 │ 1 839 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 353 kB │ 353 kB │ 366 kB │ 391 kB │ 369 kB │ 9.91 kB │ 353 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ Req/Bytes counts sampled once per second. # of samples: 10 12827 2xx responses, 6423 non 2xx responses 19k requests in 10.07s, 3.69 MB readThroughput rose 50%, from 1 284 to 1 925. Mean latency fell 34%, from 15.11 ms to 9.96 ms. On a dashboard of those two numbers this release wins, and a third of its callers received 503.
Two cells give it away.
Latency2.5%reads0 mswhile50%still reads 15 ms, so a large block of responses cost nothing.Stdevwent from 2.24 ms to 7.32 ms without the median moving, the signature of two populations rather than one spread.The line that names it is
12827 2xx responses, 6423 non 2xx responses. autocannon prints it only when a non-2xx response arrives, so its absence in step 3 was the healthy case.The median stayed at 15 ms in both runs. No successful request was faster. The mean fell because the failures were free.
- Step 5.
Read the counters that neither table prints.
curl -s http://127.0.0.1:9670/reset > /dev/null && npx autocannon@8 -c 20 -d 10 -j http://127.0.0.1:9670/failing > run.json && node -e "const r=require('./run.json');console.log('rps ',r.requests.average);console.log('latency',r.latency.average,'mean',r.latency.p50,'p50');console.log('2xx ',r['2xx']);console.log('non2xx ',r.non2xx,JSON.stringify(r.statusCodeStats));console.log('errors ',r.errors,' timeouts',r.timeouts,' resets',r.resets,' mismatches',r.mismatches)"rps 1941.9 latency 9.89 mean 15 p50 2xx 12940 non2xx 6479 {"200":{"count":12940},"503":{"count":6479}} errors 0 timeouts 0 resets 0 mismatches 0statusCodeStatsis the only place the report names the status that arrived.errors,timeoutsandresetscount requests that produced no status at all, so a run can report zero non-2xx and still have failed completely.requests.averagehere is 1941.9, a plain number. The printed table showed1 925with a non-breaking space, and51,94with a decimal comma. Compute with the JSON. - Step 6.
Stop the target and confirm the port is clear.
netstat -ano | grep "127.0.0.1:9670 " | grep LISTENING; powershell -Command "Stop-Process -Id 38676 -Force"; sleep 1; netstat -ano | grep ":9670 " | grep -c LISTENINGTCP 127.0.0.1:9670 0.0.0.0:0 LISTENING 38676 0Stop the one process id you started. Stopping every
node.exetakes other people's servers with it.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| A 2xx responses, non 2xx responses line | Some responses carried an error status | Stop reading the tables. Every figure in them counts those responses as successes. |
| No such line at all | Every response was 2xx | The tables describe the whole run. |
| Latency Avg below 50% | A block of responses cost near zero | Look for fast failures or a cached branch, then read statusCodeStats. |
| Latency Stdev larger than Avg | Two populations, not one spread | Report percentiles instead of the mean, and find what separates the two. |
| Req/Sec Min far below Avg | One or more seconds were much slower | Usually the start of the run. Measure the first seconds on their own. |
| errors or timeouts above zero in -j | Requests that never got a status | The rate and the latency now describe only the requests that finished. |
Common mistakes
What to check next
- Error rate in performance testing: the failures that never reach the non-2xx count.
- How to check p95 latency: the percentile this table never prints.
- How to check requests per second an api can handle: how far throughput moves between identical runs.
- Load test ramp up time: why the slowest second is usually the first.
- How to test concurrent users: the same tables read as a curve across connection counts.
FAQ
What is in a sample load test report?
Two tables and a footer: a latency distribution over requests, a throughput distribution over one sample per second, and a summary line with the request total and bytes read. A third line, 2xx against non-2xx, appears only when something failed.
Which performance testing metrics matter?
A latency percentile, the throughput at a named concurrency, and the count of responses that were not 2xx. The first two are meaningless without the third. Record the connection count and the run length beside them.
How do I analyze a load test report?
Check the failure count first, then the percentiles, then the throughput. Compare against a previous run of the same command on the same target, never against a figure from another machine or tool.
Verified
Verified by Maks Vernyautocannon 8.0.0node 22.23.2curl 8.1.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate12 minpublished updated Maks Verny