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

// 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'));

Steps

  1. Step 1.

    Confirm the port has no listener.

    netstat -ano | grep ":9670 " | grep -c LISTENING
    
    0

    Any other number means another process owns the port.

  2. Step 2.

    Start the target.

    node results-server.mjs > server.log 2>&1 & sleep 2; netstat -ano | grep ":9670 " | grep LISTENING
    
      TCP    127.0.0.1:9670         0.0.0.0:0              LISTENING       38676

    The last column is the process id for step 6.

  3. Step 3.

    Run the load against the healthy route.

    npx autocannon@8 -c 20 -d 10 http://127.0.0.1:9670/ok
    
    Running 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 read

    The 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. The Req/Sec row is a distribution over 10 numbers, one per second, which # of samples: 10 states. Its Avg, 1 284, is the throughput figure.

    The last column differs on purpose. Latency ends at Max and throughput at Min, because the worst second is the smallest one. Neither table has a column for failures, and the footer counts completed responses of any status.

  4. 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/failing
    
    Running 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 read

    Throughput 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. Latency 2.5% reads 0 ms while 50% still reads 15 ms, so a large block of responses cost nothing. Stdev went 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.

  5. 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 0

    statusCodeStats is the only place the report names the status that arrived. errors, timeouts and resets count requests that produced no status at all, so a run can report zero non-2xx and still have failed completely.

    requests.average here is 1941.9, a plain number. The printed table showed 1 925 with a non-breaking space, and 51,94 with a decimal comma. Compute with the JSON.

  6. 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 LISTENING
    
      TCP    127.0.0.1:9670         0.0.0.0:0              LISTENING       38676
    0

    Stop the one process id you started. Stopping every node.exe takes 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

Sign: A report shows throughput up and latency down, and the service got worse.Cause: A failure that returns immediately is counted as a fast response. The run above went from 1 284 to 1 925 requests a second and from 15.11 ms to 9.96 ms mean latency while returning 503 to 6 423 callers. Compare 2xx counts between two reports before comparing any rate.
Sign: The percentiles in the two tables are compared with each other.Cause: They have different populations. The latency row is a percentile over every request. The Req/Sec row is a percentile over one sample per second, ten of them in a ten second run, which the line # of samples states. A 97.5% throughput cell backed by ten numbers is the second best second.
Sign: A report is summarised as a single average response time.Cause: The mean is the one figure a failing run improves. Here the mean moved 5 ms while the median stayed at 15 ms in both runs. Quote a percentile and a success count together, because neither survives alone.

What to check next

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.

intermediate12 minpublished updated Maks Verny