Your first test
A complete YAML load test with a ramping profile, a captured variable, jittered think time and CI thresholds — explained line by line.
This is a complete test. Save it as test.yaml, point baseURL at something of
your own, and run it.
name: storefront
baseURL: https://staging.example.com
load:
executor: ramping-vus
stages:
- { duration: 30s, target: 100 } # ramp up
- { duration: 5m, target: 100 } # hold
- { duration: 30s, target: 0 } # ramp down
# Pause after every request, whatever its outcome. Defaults to 1s, which keeps
# a scenario — or a failing endpoint — from being hammered in a tight loop.
# Set it to "0" for a throughput test.
betweenRequests: 500ms-1s
thresholds:
- { metric: http_req_duration, stat: p95, op: "<", value: 500 }
- { metric: http_req_failed, stat: rate, op: "<", value: 0.01 }
scenarios:
- name: browse
steps:
- name: list products
get: /api/products
expect: [200]
capture:
productId: items.0.id # pull a value out of the JSON response
- think: 1s-3s # jittered, like a real person reading the page
- name: view product
get: /api/products/${productId} # ...and use it here
expect: [200]Check it before running it
loadwave validate test.yamlThis parses the file, resolves its scenarios and describes what it would do, without generating any load. It catches a misspelled field, a scenario name that does not exist in this binary, or a threshold on a metric that will never be produced — in milliseconds, rather than after you have waited out a real run.
Unknown fields are rejected, not ignored. A misspelled key in a load test is an expensive kind of bug: the run appears to work and quietly measures something other than what you asked for.
loadwave validate belongs in a pre-commit hook or a pull-request check.
Read the exit code
0 means every threshold passed. 2 means the run completed but a threshold
was breached. 1 means LoadWave itself could not do what was asked. That
distinction is what makes it usable as a CI gate.
What each part is doing
load — the shape over time
load:
executor: ramping-vus
stages:
- { duration: 30s, target: 100 }
- { duration: 5m, target: 100 }
- { duration: 30s, target: 0 }ramping-vus moves linearly between stage targets. Every profile starts from
zero, so the first stage ramps up rather than starting at its own target. The
alternative is constant-vus, which holds a fixed number and needs either a
duration or an iterations budget.
Ramping down at the end is not decoration: it lets in-flight iterations finish instead of manufacturing a cliff of cancelled requests that then appear as failures in the results you are about to read.
See the load reference for every field.
betweenRequests — the pacing floor
betweenRequests: 500ms-1sA pause inserted after every request, whatever its outcome. It defaults to one second and it is deliberately not zero. Without it:
- a scenario with no think time of its own loops as fast as the network allows;
- a scenario whose request fails instantly — a refused connection, a 500 from a cache — loops as fast as the CPU allows.
The second is the dangerous one. It is how a load test turns into an accidental denial of service against a service that has already fallen over.
Prefer a range. Identical pauses make every virtual user march in lockstep, producing traffic in synchronised bursts rather than the smooth arrival pattern a real population generates — and the bursts are what your service ends up being measured against.
capture — carrying a value between steps
capture:
productId: items.0.idPulls a value out of the JSON response and makes it available as ${productId}
in later steps. Field access and array indexing, in dotted or bracketed form;
deliberately not full JSONPath.
Unknown names render as empty strings, so a capture that did not fire produces a request that visibly misses rather than an iteration that dies before making one.
think — a pause, jittered
- think: 1s-3sAlways a range, never a fixed value. Constant think times make virtual users march in lockstep and produce artificial traffic spikes that no real population would.
Think time is excluded from iteration_duration, so it does not distort the
metric, and it is interruptible, so a stopping run does not sit through
everyone's pause.
thresholds — the pass/fail contract
thresholds:
- { metric: http_req_duration, stat: p95, op: "<", value: 500 }
- { metric: http_req_failed, stat: rate, op: "<", value: 0.01 }Durations are in milliseconds; rates are fractions of one. Thresholds are evaluated against whole-run aggregates, and a breach latches — a p95 that recovers by the end of the run still breached.
A threshold on a metric that was never produced is reported as not measured, not as a pass. See Thresholds.
Overriding it from the command line
Flags win over the file, which is how one file serves several environments:
loadwave run test.yaml --url https://prod-canary.example.com
loadwave run test.yaml --vus 500 --duration 10m
loadwave run test.yaml --tag env=prod --threshold 'http_req_duration:p99<1000'