Using it in CI
The exit-code contract, keeping results as artefacts, and a GitHub Actions workflow that gates on a load test without becoming flaky.
LoadWave is built to be a pipeline step. Thresholds are first-class, and the exit code says which kind of failure you got.
Exit codes
The run completed and every threshold passed.
LoadWave could not do what was asked: bad configuration, no agents, unreachable coordinator.
The run completed, but a threshold was breached.
Ctrl-C, or a SIGINT from whatever supervises the process.
The distinction between 1 and 2 is deliberate: "the tool broke" and "the
service was too slow" call for very different responses from a pipeline. A 1
usually means somebody should look at the workflow; a 2 means somebody should
look at the service.
A GitHub Actions workflow
name: Load test
on:
pull_request:
schedule:
- cron: "0 3 * * *"
jobs:
load:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.26.6"
- name: Install LoadWave
run: go install github.com/SnowyFoxStudios/LoadWave/cmd/loadwave@latest
# Catches a misspelled field or a threshold on a metric that will never
# be produced, in milliseconds rather than after a ten-minute run.
- name: Validate configuration
run: loadwave validate test.yaml
- name: Load test
run: loadwave run test.yaml --out results.json --report results.html
- name: Keep the results
if: always()
uses: actions/upload-artifact@v4
with:
name: load-test
path: results.*if: always() on the upload step is the important detail. The run that failed is
precisely the one whose results somebody needs to read.
The two output files
loadwave run test.yaml --out results.json --report results.html--out writes the whole snapshot as JSON — every metric total, per-endpoint
aggregate, threshold verdict and event. That is the one to parse if a later step
needs to make a decision or post a comment.
--report writes a self-contained HTML file with the charts included as inline
SVG: no scripts, no external assets, no network dependency. It renders the same
as a build artefact, in an email, as a ticket attachment, or opened in two years
to settle an argument about when a regression started. See
Reports.
Distinguishing the two failures in a script
set +e
loadwave run test.yaml --out results.json --report results.html
code=$?
set -e
case "$code" in
0) echo "::notice::Load test passed" ;;
2) echo "::error::A threshold was breached — see the report artefact" ;;
*) echo "::error::LoadWave failed to run (exit $code)" ;;
esac
exit "$code"Keeping it from becoming flaky
A load test in CI that fails half the time gets disabled within a month. Four things prevent that.
Validate before you run
loadwave validate test.yaml costs milliseconds and catches the whole class of
failures that would otherwise waste the run — an unknown field, a scenario name
this binary does not have, a threshold on a metric that will never be produced.
Put it in a pre-commit hook as well as the pipeline.
Pin the version
go install github.com/SnowyFoxStudios/LoadWave/cmd/loadwave@v0.4.0@latest means a threshold can start failing because the tool changed rather than
because your service did. That is exactly the sort of ambiguity a gate must not
have.
Set thresholds from a baseline, not from hope
Run the test a few times against a known-good build, look at where the p95 actually sits, and set the threshold above the noise. A threshold chosen because 500ms is a round number will either never fire or always fire.
Give the run a machine that is not busy
A shared runner already compiling something else produces latency measurements that describe the runner. If the numbers matter, run the generator somewhere dedicated and point it at the environment — see Distributed runs.
Gating a deployment rather than a pull request
The same command, with abortOnFail doing the heavy lifting so a broken canary is
caught in seconds rather than after the full profile:
thresholds:
- { metric: http_req_duration, stat: p95, op: "<", value: 500 }
- { metric: http_req_failed, stat: rate, op: "<", value: 0.01 }
- { metric: http_req_failed, stat: rate, op: "<", value: 0.25, abortOnFail: true }loadwave run canary.yaml --url "https://$CANARY_HOST" --duration 3m \
--tag release="$GIT_SHA" --out results.json || rollbackTagging the run with the release means the results file identifies what was measured, which matters as soon as you keep more than one of them.
Waiting for a fleet
When the load comes from machines the pipeline starts, run can wait for them
rather than beginning with whoever happened to connect first:
loadwave run test.yaml --agents 4 --wait-agents 2mIt exits 1 if fewer than four agents have joined within two minutes — a tool
failure, correctly, rather than a quiet run at a quarter of the intended load.