surgesurge

Reading the report

-html report.html writes one self-contained file: no server, no CDN, no internet. Open it in six months and it still draws. This is the order to read it in, and what each part is for.

1. The bar, and what kind of test this was

Pinned to the top: the scenario, the kind of test, the host, the verdict, peak users, workers, throughput and failure rate. A ? beside the kind means surge inferred it from the shape of the load rather than being told — declare type: and the caveat goes away.

The kind decides what a verdict means before any number does. FAIL on a stress run is the result, not a regression. A soak that passed a whole-run p95 has proved nothing about the last hour, because the average of a healthy first hour and a sick last one is fine. The Run tab spells out what the run was judged on.

2. The verdict and its reasons

text
FAIL  2 of 3 thresholds breached · 4,126 requests · 83 failed (2.01%)
  ✗ threshold error_rate < 1% — actual 2.01%
  ⚠ feeder "accounts" has 80 rows for 110 virtual users

3. The ⚠ caveats, before any number below them

They do not change the verdict. They decide whether it is worth anything.

CaveatWhat it means for your conclusion
no thresholds were setthe run could not fail. A pass means it finished
stopped earlythe numbers describe the part that ran, not the plan
N arrivals could not be startedthe rate on the chart was never applied
generator CPU peaked at…you measured this machine, not the target
N extracts failedsome users stopped mid-journey; later steps are under-represented
feeder has fewer rows than VUsseveral users shared an identity; you may be measuring row contention

If a caveat invalidates the run, say so first and do not interpret the numbers underneath. A confident reading of a bad run is worse than no reading.

4. Failures, with the evidence

Each distinct failure keeps one real example — the role, the URL, the status, which checks gave way, and what the server replied:

text
POST api/checkout (buyer)          check failed     ×113
  check: json status = confirmed
  replied: {"order_id":"o-10826","status":"pending","total":30.5}

That one is invisible to the error rate: it answered 200 and answered wrongly. Quote the body. "113 checkout failures" is a statistic; the body is evidence.

Failures are grouped by signature and counted, so a million of them cost the memory of fifty.

5. Checks, by name

check_rate 97.1% is not a place to look. json status = confirmed — 113 of 799 failed is.

6. User actions

If the scenario used group:, this is the table the business asked for:

text
user actions  (end to end, think time excluded)
action                              ran   failed      avg      p95      p99
checkout                            842       40     60.4     82.9     87.0

It is not part of the totals — every request inside an action is already counted there — so do not add it to the throughput, and do not compare its p95 with a request's. An action fails when any step in it failed, so "40 failed" can be forty different steps.

7. Who did the work

Planned share against the share that actually ran:

roleplannedactualiterations
shopper64.2%52.9%1,044
buyer32.1%42.6%842
back-office3.6%4.5%89

A gap like that is arithmetic, not a bug: weight decides how many users pick a role, not how fast they iterate. A role that thinks for a second completes fewer iterations than one that does not, whatever its weight. Every average in the report is weighted by the actual column. If the mix you measured is not the mix you meant, the overall p95 describes a system nobody runs — change the think time or the weights, not the conclusion.

8. The log

Timestamped: feeder warnings, generator saturation, an abort rule firing, a node joining late. Line it up against the charts — "p95 climbed at 00:42" and "the generator saturated at 00:41" is one finding, not two. On a merged fleet run each line says which node it came from, or each node: when they all said it.

9. Where the time went

With -timings, the dominant phase names the suspect:

PhaseWhat it means
waitthe server thinking. The application's own time — the normal answer
connect / tlsconnections are not being reused: keep-alive, a proxy, or a pool too small
blockedyour own client pool is the limit, not the server
dnsresolution per request. Almost always a client bug

Read reuse% too. Below 90% the setup cost is inflating every latency above it.

10. What the target said about itself

With -monitor, the server-side series ranked by how closely each moved with the latency. Correlation ranks where to look; it is not cause. Under load almost everything moves together.

11. Throughput against latency over time

The knee — throughput flat while latency climbs — is the capacity limit. Past it, adding users adds queueing, not work. The three charts share one time axis so the knee is visible rather than inferred.

Long lists

Every long table — failures, checks, exceptions, agents, server-side metrics, requests — has a search box and a show all. A report of two thousand endpoints is still one file you can find one endpoint in.

The other files

FlagFileFor
-jsonsummary.jsonthe same structure the library's Run returns; what compare, analyze and baseline save read
-csvtimeline.csvone row a second: sec,reqs,errs,avg,p50,p95,max,vus
-failuresfailures.csvone row per distinct failure, with its example. For grepping and diffing
-snapshotraw.jsonmergeable histogram buckets, for surge merge
-log-requestsrequests.csvone line per request, for outlier forensics
-prometheus -influx -webhook—the same run, in whatever your dashboards already read

Upload the HTML report on failure too. The report is the thing that explains the failure; a pipeline that throws it away on red has kept the one artefact nobody needs.