LoadWave
Getting started

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.

test.yaml
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.yaml

This 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.

Run it

loadwave run test.yaml

Or watch it happen:

loadwave run test.yaml --ui

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-1s

A 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.id

Pulls 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-3s

Always 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'

Where to go from here

On this page