LoadWave

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: AGPL-3.0

README

LoadWave

Distributed load testing with a live dashboard.

Write your test in Go or YAML, run it from one machine or fifty, and watch the results as they happen.

CI Go Reference Docs


What it is

LoadWave generates HTTP load from a fleet of worker processes, merges what they measure into one coherent picture, and shows it live in a browser — or prints a report and exits non-zero, if that is what your pipeline needs.

It is one binary. No agent to deploy alongside it, no database to stand up, no separate UI to host.

loadwave demo                     # nothing to set up — see it working now
loadwave run test.yaml            # run your test, print a report, exit
loadwave run test.yaml --ui       # ...and watch it in a browser

Why another one?

  • Percentiles that are actually correct across machines. Nodes ship whole HDR histograms rather than their own percentiles, and the coordinator merges the distributions. A p99 from ten agents is the real p99, not an average of ten p99s — which is not a number that means anything.
  • Real processes, not just goroutines. Past a few thousand virtual users a single Go runtime's scheduler and garbage collector become the bottleneck rather than the system under test. LoadWave spreads the pool across worker processes so you keep measuring the server.
  • Tests you can review. Behaviour is ordinary Go — conditionals, session state, custom auth — testable with go test against an httptest.Server, with no coordinator involved. Simple flows can stay in YAML.
  • Built to be a CI gate. Thresholds are first-class, and the exit code distinguishes "the tool broke" from "your service was too slow".

Install

go install github.com/SnowyFoxStudios/LoadWave/cmd/loadwave@latest

Or grab a binary from Releases. Every archive is one self-contained executable with the dashboard inside it.

Building from source needs Go 1.26+ and Node 26+:

git clone https://github.com/SnowyFoxStudios/LoadWave.git
cd LoadWave
make build      # ./bin/loadwave

See it working

loadwave demo starts a small target server of its own and runs a two-scenario test against it, with the dashboard on http://localhost:8088. No configuration, no service to stand up:

loadwave demo                        # runs for 10 minutes; Ctrl-C to stop early
loadwave demo --duration 2m --vus 50
loadwave demo --headless             # no dashboard, just the report

The demo target is a toy — its numbers say nothing about your hardware. It is there to show you the dashboard and to prove a fresh build works end to end.

Your first test

# 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]
loadwave run test.yaml
──────────────────────────────────────────────────────────────────────────────
  storefront — completed
  30s to 100 VUs, then 5m0s to 100 VUs, then 30s to 0 VUs
  ran for 6m0s across 1 agent
──────────────────────────────────────────────────────────────────────────────

  METRIC             VALUE
  requests           184,209
  throughput         512/s
  duration p95       46.3ms
  failed requests    2,931 (1.59%)
  checks passed      181,278 of 184,209

  REQUEST          COUNT    AVG     P95     P99     MAX     ERRORS
  view product     92,104   30.3ms  48.6ms  50.4ms  91.2ms  3.18%
  list products    92,105   15.1ms  24.7ms  25.5ms  62.1ms  0%

  THRESHOLD                    ACTUAL  RESULT
  http_req_duration p95 < 500  46.34   pass
  http_req_failed rate < 0.01  0.0159  FAIL

Exit code 2 — a threshold was breached. Your pipeline just caught a regression.


The dashboard

loadwave demo                      # a self-contained tour
loadwave run test.yaml --ui        # dashboard alongside a scripted run
loadwave serve                     # long-lived; start runs from the browser

Live charts for virtual users, throughput, response time and responses by status class. The response-time chart draws one line per endpoint — click any of them, or any row of the Requests table, to isolate it.

Below the charts: a per-endpoint breakdown, threshold verdicts, agent health, an event log, and a Failed requests panel giving each failure's status code and an excerpt of what the server actually said. Runs can be started, stopped and rescaled while they are in flight — rescaling takes a ramp, so raising the target introduces the new users over a period rather than spawning them all in one tick.

Stopping a run does not stop LoadWave. The run ends, the results stay on screen, and you can download the report or start another test. Ending the process is a separate, deliberate action: Ctrl-C in the terminal, or the Power off control in the browser.

The whole surface is also a REST API, so anything the dashboard does is scriptable without it.

Building a test in the browser

New run opens a scenario builder: a form for the load profile, pacing, thresholds and scenario steps, with the YAML generated beside it as you type.

It is not an alternative format. The panel on the right is the file you would have written by hand, so Copy gives you something to commit and run from CI with loadwave run. Switch to Edit YAML at any point to hand-tune the result, or to paste a file you already have.

Every edit is checked by the same parser the runner uses — the builder does not reimplement the schema — and the answer is echoed back in words:

Profile      30s to 25 VUs, then 2m0s to 25 VUs, then 30s to 0 VUs
Peak VUs     25
Pacing       1s (default)
Scenarios    browse ×1
Thresholds   http_req_duration p95 < 500 · http_req_failed rate < 0.01

That is a stronger confirmation than a green tick: it proves the runner read the form the same way you did. A test with no thresholds is called out, since it will pass whatever the results turn out to be. Start run stays disabled until the configuration actually parses.

Taking the results away
loadwave run test.yaml --report results.html

Or use the Download report button. Either way you get one self-contained HTML file — charts included, as inline SVG — with no scripts, no external assets and no network dependency. It renders the same in an email client, as a ticket attachment, or opened in two years to settle an argument about when a regression started.


Going distributed

One machine can usually push more load than people expect. When it cannot, nothing about the test changes — only where it runs.

On the coordinating host:

loadwave serve --listen 0.0.0.0:8090

On each load-generating host:

loadwave agent --coordinator loadgen-controller:8090 --workers 8

Agents dial out, so they need no inbound ports and work from behind NAT. The coordinator divides the load in proportion to the capacity each agent advertises, and an agent that joins mid-run is put to work immediately — so scaling a running test out is just starting another agent. If one dies, the survivors absorb its share and the dashboard says so.


Writing tests in Go

YAML runs out of road as soon as a test needs a session, a branch, or an authentication flow that isn't a bearer token. At that point write Go: your program becomes the LoadWave binary, so there is nothing extra to deploy.

package main

import (
    "context"
    "net/http"
    "time"

    "github.com/SnowyFoxStudios/LoadWave/pkg/loadwave"
    "github.com/SnowyFoxStudios/LoadWave/pkg/loadwave/run"
)

func main() {
    loadwave.Register(loadwave.Scenario{
        Name:      "checkout",
        OnVUStart: signIn, // once per virtual user, not once per iteration
        Run:       checkout,
    })
    run.Main()
}

func checkout(ctx context.Context, vu *loadwave.VU) error {
    resp, err := vu.HTTP().PostJSON(ctx, "/api/orders", order{Payment: "card"})
    if err != nil {
        return err
    }
    vu.Check("order accepted", resp.StatusCode == http.StatusCreated)
    vu.ThinkBetween(ctx, time.Second, 3*time.Second)
    return nil
}
go build -o checkout ./cmd/checkout
./checkout run --url https://staging.example.com --vus 200 --duration 5m --ui
./checkout agent --coordinator loadgen-controller:8090   # the same binary

Because the scenarios are compiled in, every host in the fleet is provably running the same test. See examples/checkout for a worked example with login, per-user state and a custom metric.

Scenarios are ordinary Go, so they are testable without any LoadWave infrastructure at all:

func TestCheckout(t *testing.T) {
    server := httptest.NewServer(myHandler())
    defer server.Close()

    factory, _ := loadwave.NewHTTPClientFactory(loadwave.HTTPOptions{BaseURL: server.URL})
    vu := loadwave.NewVU(loadwave.VUConfig{ID: 1, HTTP: factory.New()})

    if err := checkout(t.Context(), vu); err != nil {
        t.Fatal(err)
    }
}

In CI

- 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.*
Exit code Meaning
0 The run completed and every threshold passed.
1 LoadWave could not do what was asked: bad configuration, no agents, unreachable coordinator.
2 The run completed, but a threshold was breached.
130 Interrupted.

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.


Commands

Command What it does
loadwave demo Run a self-contained demo against a built-in target. Nothing to configure.
loadwave run Run a test to completion and print a report. Add --ui for the dashboard.
loadwave serve Long-lived coordinator and dashboard; runs are started from the browser or the API. Pass --allow-shutdown to expose the browser's Power off control.
loadwave agent Join a coordinator and generate load on this machine.
loadwave validate Check a configuration without running it. Belongs in a pre-commit hook.
loadwave version Print version and build information.

loadwave <command> --help has the flags.


Documentation

The full documentation site is loadwave.snowyfoxgames.com — getting started, guides for the dashboard, distributed runs, thresholds and the Go SDK, and the complete REST API and configuration reference.

The same material also lives in this repository:

Configuration reference Every field of the YAML format.
Metrics What is measured, and how it is aggregated.
Architecture How the coordinator, agents and workers fit together.
Distributed runs Running across machines, and what happens when one dies.
Go SDK API reference.
Contributing How to build, test and submit changes.

Status

Early. The architecture is settled and the whole path — coordinator, agents, worker processes, merged metrics, dashboard — works and is covered by tests that spawn real subprocesses. The API may still change before v1.0.0.

Protocols other than HTTP, an open-model arrival-rate executor, and persisting results beyond a run are on the roadmap. Issues and pull requests welcome.

License

AGPL-3.0-or-later. Using LoadWave to test your own systems carries no obligations. Offering a modified LoadWave to others as a network service means publishing your changes.

Directories

Path Synopsis
cmd
loadwave command
Command loadwave is the standalone LoadWave binary.
Command loadwave is the standalone LoadWave binary.
examples
checkout command
Command checkout is a load test written with the LoadWave Go SDK.
Command checkout is a load test written with the LoadWave Go SDK.
gen
internal
agent
Package agent runs one host's share of a distributed load test.
Package agent runs one host's share of a distributed load test.
apportion
Package apportion divides an integer total across weighted claimants.
Package apportion divides an integer total across weighted claimants.
buildinfo
Package buildinfo reports what this binary is.
Package buildinfo reports what this binary is.
cli
Package cli implements the loadwave command line.
Package cli implements the loadwave command line.
control
Package control carries commands down and telemetry up between the tiers of a LoadWave cluster.
Package control carries commands down and telemetry up between the tiers of a LoadWave cluster.
coordinator
Package coordinator is LoadWave's control plane.
Package coordinator is LoadWave's control plane.
engine
Package engine executes a load profile inside one worker process.
Package engine executes a load profile inside one worker process.
httpapi
Package httpapi serves the LoadWave dashboard and its REST and WebSocket interfaces.
Package httpapi serves the LoadWave dashboard and its REST and WebSocket interfaces.
idspace
Package idspace partitions the virtual user id range across a cluster.
Package idspace partitions the virtual user id range across a cluster.
procstats
Package procstats reports a process's own resource usage — CPU time and resident memory — for nodes to include in their heartbeats.
Package procstats reports a process's own resource usage — CPU time and resident memory — for nodes to include in their heartbeats.
report
Package report renders a finished run as a single self-contained HTML file.
Package report renders a finished run as a single self-contained HTML file.
scenario
Package scenario reads LoadWave's YAML configuration and turns it into the two things the rest of the system needs: a wire-format test plan, and — for tests written declaratively rather than in Go — a set of runnable scenarios.
Package scenario reads LoadWave's YAML configuration and turns it into the two things the rest of the system needs: a wire-format test plan, and — for tests written declaratively rather than in Go — a set of runnable scenarios.
worker
Package worker is the process that actually generates load.
Package worker is the process that actually generates load.
pkg
loadwave
Package loadwave is the public API for writing LoadWave load tests in Go.
Package loadwave is the public API for writing LoadWave load tests in Go.
loadwave/run
Package run turns a Go program into a complete LoadWave binary.
Package run turns a Go program into a complete LoadWave binary.
Package web embeds the built dashboard into the LoadWave binary.
Package web embeds the built dashboard into the LoadWave binary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL