Thresholds
Turning a run into a verdict — how thresholds are evaluated, why a breach latches, and what "not measured" means.
A threshold is an assertion about the whole run. Breach any one of them and
LoadWave exits 2, which is what makes it usable as a gate rather than a report
somebody has to read.
Writing them
thresholds:
- { metric: http_req_duration, stat: p95, op: "<", value: 500 }
- { metric: http_req_failed, stat: rate, op: "<", value: 0.01 }
- { metric: checks, stat: rate, op: ">", value: 0.99 }| Field | Meaning |
|---|---|
metric | Any metric name, built-in or custom. |
stat | count, rate, avg, min, max, p50, p90, p95, p99, p999. |
op | <, <=, >, >=. |
value | Milliseconds for durations; a fraction of one for rates. |
abortOnFail | Stop the run immediately on breach rather than only failing at the end. |
Or from the command line, which is how you vary one number per environment without editing the file:
loadwave run test.yaml --threshold 'http_req_duration:p95<500'
loadwave run test.yaml --threshold 'http_req_failed:rate<0.01'Three properties worth understanding
They are evaluated against whole-run aggregates
Not against the last tick, not against a rolling window. That is the question a CI gate is asking: did this build meet the bar across the run I just did?
A breach latches
A p95 that spikes at minute three and recovers by minute ten still breached. A gate looking only at the final instant would let a real regression through — the run got slow, and the fact that it got better again does not unmake that.
"Not measured" is not a pass
A threshold on a metric the run never produced is reported as not measured, and it does not silently succeed.
"We never measured it" and "it was fine" are very different answers to hand back
to a pipeline. This is the failure mode that lets a threshold on a typo'd metric
name sit green in CI for a year. loadwave validate catches most of them before
a run ever starts.
abortOnFail
thresholds:
- { metric: http_req_failed, stat: rate, op: "<", value: 0.25, abortOnFail: true }Stops the run the moment the condition holds, rather than waiting for the end.
Use it for conditions where finishing teaches you nothing. An error rate of 25% means the environment is broken, not slow — there is no information in the next nine minutes of hammering it, and there is a real cost to a load test continuing against a service that has already fallen over.
Keep the abort threshold well clear of the pass/fail one. A pair like this reads correctly:
thresholds:
# The bar this build has to clear.
- { metric: http_req_failed, stat: rate, op: "<", value: 0.01 }
# The point at which continuing is pointless.
- { metric: http_req_failed, stat: rate, op: "<", value: 0.25, abortOnFail: true }Thresholds on custom metrics
A metric emitted by a Go scenario is treated exactly like a built-in one:
vu.Metrics().Trend("cart_value", vu.Labels(), receipt.Total)thresholds:
- { metric: cart_value, stat: p50, op: ">", value: 0 }That particular one is a useful trick: it asserts the scenario is actually completing purchases, not just returning quickly. A test where every checkout 500s in two milliseconds passes a latency threshold beautifully.
A starting set
Worth having on almost every test:
thresholds:
# Latency, at a percentile that reflects the experience rather than the average.
- { metric: http_req_duration, stat: p95, op: "<", value: 500 }
# Errors. Note the rate is a fraction of one, so this is 1%.
- { metric: http_req_failed, stat: rate, op: "<", value: 0.01 }
# Checks — this is what catches "fast, but wrong".
- { metric: checks, stat: rate, op: ">", value: 0.99 }
# Stop early when the environment is broken rather than slow.
- { metric: http_req_failed, stat: rate, op: "<", value: 0.25, abortOnFail: true }Prefer p95 or p99 over avg. An average hides exactly the tail that users
notice, and it is the statistic most likely to stay green through a regression
that a percentile would catch immediately.
What the verdict looks like
THRESHOLD ACTUAL RESULT http_req_duration p95 < 500 46.34 pass http_req_failed rate < 0.01 0.0159 FAIL
The same table appears in the dashboard and in the HTML
report, and the structured form is in --out results.json
for anything that needs to read it programmatically.