LoadWave
Guides

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

0Success

The run completed and every threshold passed.

1Tool failure

LoadWave could not do what was asked: bad configuration, no agents, unreachable coordinator.

2Threshold breached

The run completed, but a threshold was breached.

130Interrupted

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

.github/workflows/load-test.yml
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 || rollback

Tagging 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 2m

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

Next

On this page