Reading results¶
A load test's failure mode is not a crash — it is a plausible number that is wrong. This page is the order in which a run must be read, and what every field means.
Read it in this order¶
1. generator_ok false → STOP. Discard the run. Nothing below means anything.
2. target_unreachable true → connectivity, not capacity. Not a knee.
3. aborted true → you found the knee. That is a success.
4. per class: the share past guillotine_ms ← your margin
5. only then: latency, errors, cache
1. generator_ok: false → discard the run¶
More than 2% of iterations were dropped: k6 could not start them at the requested rate, so the bottleneck was the generator or its network, not the system under test.
This is the single most common way to get a confidently wrong answer out of a load test, because such a run looks exactly like a healthy system absorbing the load: low latency, no errors, a rate that seems fine. It is not a threshold to tune. Move the generator closer to the target, or onto a bigger host, and repeat — and if you were running through Docker on a macOS or Windows laptop, that is the cause.
2. target_unreachable: true → connectivity¶
Over 90% failed and p95 under 50 ms. A saturated system is slow before it errors — a real knee shows
up as latency climbing into the timeout. Near-zero latency with near-total failure means connections
refused or never routed: wrong address, wrong port, TLS, firewall, or a container whose network namespace
does not reach the target. Reporting that as "the brake found the knee" would hand out a capacity number
for a target nobody touched. Run crowdsim probe before trying again.
3. aborted: true → the knee, and that is the point¶
A threshold with abortOnFail fired: the brake stopped the run. The exit code stays 0, because this is
the outcome the tool exists to produce. Holding a system in collapse hurts real users and adds no
information.
4. The share past guillotine_ms, per class¶
guillotine_ms is your reverse proxy's read timeout. Requests slower than it become 504s for real
visitors, so the interesting column is not the average latency but the percentage that crossed it:
class target req/s p50 p95 p99 >SLO failed
─────────────────────────────────────────────────────────────────────────
rsc_page 25.9 140 ms 900 ms 1600 ms 0.31% 0.10%
html 13.9 90 ms 500 ms 800 ms 0.00% 0.00%
rsc_search 13.7 380 ms 4200 ms 8900 ms 4.80% 0.20%
That 4.80% is the margin: at this rate, roughly one search in twenty is already a 504 for somebody. Averages hide the queue, and the queue is what produces the errors.
summary-<run_id>.json, field by field¶
Run identity¶
| Field | Meaning |
|---|---|
run_id |
UTC timestamp, e.g. 20260805T093710Z. Also the X-Crowdsim-Run header on every request, so you can find the test in your own access logs. |
profile |
The profile's name, not its filename. |
shape |
mix or journey. |
base_url |
What was actually hit. |
rsc_mode |
repeat or random. Runs with different modes are not comparable. |
peak_rps_user_target |
The --peak you asked for, in total user req/s. |
Verdicts¶
| Field | Meaning |
|---|---|
aborted |
A real threshold failed — the brake stopped the run. (Thresholds ending in >=0 are decoration used to surface per-class sub-metrics; they are excluded.) |
generator_ok |
dropped_iterations ≤ 2% of requests. False invalidates everything else. |
target_unreachable |
failed_rate > 0.9 and p95 null or < 50 ms. |
Volume and latency¶
| Field | Meaning |
|---|---|
requests |
Total HTTP requests, all classes. Not page views. |
rps_avg |
Achieved rate. Compare it with peak_rps_user_target: a large gap with generator_ok: true usually means the brake cut the run short. |
failed_rate |
k6's http_req_failed — transport errors and non-2xx/3xx. |
dur.p50 / p95 / p99 / max |
Overall request duration, ms. null means no sample. |
dropped_iterations |
Iterations k6 could not start. The input to generator_ok. |
Errors and the timeout¶
| Field | Meaning |
|---|---|
guillotine_ms |
The read timeout from the profile, echoed for context. |
over_guillotine_rate |
Share of all requests slower than it. |
e504 / e502 / e5xx / e404 |
Counters. e5xx is cumulative: a 504 is also a 5xx. A 404 count that is not ~0 usually means a pool of URLs that do not exist, or a class hitting a tier that does not serve it. |
Cache¶
| Field | Meaning |
|---|---|
cache.<layer> |
Hit ratio per declared layer, 0–1, or null = never observed. |
null is not 0. A Rate with no samples would report 0%, which reads as "the cache missed everything"
when the truth may be "this layer was never in the path" or "the header name in the profile is wrong". If
every layer is null, the summary says so in words.
Per class¶
per_class.<name> carries p95, p99, med, failed, over_guillotine, reqs, cache.<layer>, and
rps_target — the rate that class was asked to produce (null in journey shape, where sessions rather
than classes are scheduled). mix_target lists the same targets in one place.
A class with reqs: 0 did not run: it was skipped, dropped for an empty pool, or does not exist in this
shape. The text table leaves it out rather than printing a row of zeroes.
history.tsv¶
One appended line per run, written by the driver — the GUI reads the same file, so runs launched from a terminal and runs launched from the page sit side by side:
Comparing runs honestly¶
- Only against each other, and only at an identical URL pool. A synthetic pool of distinct cold URLs is harsher than real traffic concentrated on a few hot keys.
- Same
rsc_mode, same shape, same target.repeatandrandomanswer different questions. - Never against a run with
generator_ok: false. - Absolute numbers are optimistic in one direction and pessimistic in another; the delta across a change is what the tool measures honestly. That is what to quote.
The GUI's history view enforces the first three: it only offers runs with the same profile, target and shape as comparable, marks invalid runs as unusable, and draws them hollow in the knee plot instead of alongside valid ones.
What is deliberately not here¶
A per-URL breakdown of which routes were slowest. That needs your edge's access log, which means privileged
access to your load balancers — outside a tool that would then want an SSH key for a production edge. The
X-Crowdsim-Run header exists so you can do that analysis in your own log stack.