Skip to content

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.

generator     ⛔ DID NOT hold: 4213 iterations dropped → RESULT INVALID

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, 01, 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:

run_id  profile  base_url  shape  peak  aborted  reqs  rps  failed  p95  e504  gen_ok
crowdsim history          # printed as a table

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. repeat and random answer 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.

See also