LoadWave
Reference

REST API

Every endpoint the coordinator serves — starting, stopping and rescaling runs, fetching snapshots and reports, and the live WebSocket stream.

The dashboard is a client of this API and nothing more, so anything it can do is scriptable. It is served on the same address as the UI — :8088 by default, --ui-addr to change it.

There is no authentication. Anyone who can reach this port can start and stop runs unless the server was started with --read-only. Run it on a trusted network. See Securing it.

Endpoints

MethodPathWhat it does
GET/api/v1/healthLiveness.
GET/api/v1/versionVersion and build information.
GET/api/v1/snapshotThe coordinator's whole current state.
GET/api/v1/agentsConnected agents.
GET/api/v1/runsKnown runs, newest first.
POST/api/v1/runsStart a run from a configuration.
GET/api/v1/runs/{id}One run's full state.
GET/api/v1/runs/{id}/configThe exact configuration the run was started from.
PUT/api/v1/runs/{id}/configSave an edit back to the file the run was started from.
POST/api/v1/runs/{id}/stopEnd a run.
POST/api/v1/runs/{id}/scaleChange a running test's peak VU count.
GET/api/v1/runs/{id}/seriesCumulative per-series aggregates.
GET/api/v1/runs/{id}/report.htmlThe self-contained HTML report, as a download.
POST/api/v1/shutdownStop the process. Only when started with --allow-shutdown.
GET/api/v1/streamWebSocket: a snapshot, then live updates.

An unmatched path under /api/ returns a JSON 404 rather than falling through to the dashboard's HTML shell — handing a script a page of HTML where it expected JSON is a miserable thing to debug.

Starting a run

curl -X POST --data-binary @test.yaml http://localhost:8088/api/v1/runs

The body is a LoadWave configuration in either YAML or JSON. Both go through the same parser, because JSON is a subset of YAML — which lets the browser post the object it already has while a CI script sends the very file it keeps in the repository.

202 Accepted
{ "runId": "r-3f8a91" }
StatusMeaning
202Accepted; the run is starting.
400The body was empty or the configuration did not parse. The message is the parser's.
409The start was rejected — no agents, or a run already in progress.
403The server is --read-only.

A 409 is the operator's problem to fix rather than a server fault, which is why it is a conflict and not a 500. One run at a time is deliberate: concurrent runs would share worker processes and network capacity, and the numbers would measure the interference.

Reading and saving a run's configuration

curl http://localhost:8088/api/v1/runs/r-3f8a91/config
200 OK
{
  "yaml": "name: checkout\nbaseURL: https://staging.example.com\n...",
  "sourcePath": "/home/ci/test.yaml",
  "draft": { "name": "checkout", "baseURL": "https://staging.example.com", "...": "..." }
}
FieldTypeMeaning
yamlstringThe exact configuration the run was started from.
sourcePathstringThe file this run was started from — empty for a Go scenario, a quick check built from flags, or a configuration posted to this API directly. There is nowhere to save an edit back to when this is empty.
draftobjectThe same configuration, in the shape the dashboard's Build form edits. Best-effort: a scenario compiled into the binary rather than declared in YAML has nothing to show here but its name and weight.

Saving an edit is only meaningful when sourcePath is non-empty:

curl -X PUT --data-binary @test.yaml http://localhost:8088/api/v1/runs/r-3f8a91/config
StatusMeaning
200Saved. The response body is {"savedTo": "<path>"}.
400The body did not parse as a configuration. Nothing was written.
404No such run.
409This run has no source file — there is nothing to save to.
403The server is --read-only.

This is what lets the dashboard's "New run" dialog reopen showing the test that is actually running, in both its YAML and Build views, and — for a run started from a file — save edits straight back to it. See The dashboard.

Stopping a run

curl -X POST http://localhost:8088/api/v1/runs/r-3f8a91/stop \
  -H 'Content-Type: application/json' \
  -d '{"graceful": true, "reason": "canary rolled back"}'
FieldTypeDefaultMeaning
gracefulbooltrueLet in-flight iterations finish.
reasonstringRecorded in the event log and the run summary.

The body is optional; omitting it stops gracefully.

graceful defaults to true because an abrupt stop manufactures a cliff of cancelled requests that then show up as failures in the very results you are about to read.

Stopping a run does not stop the process. The coordinator keeps serving, still holding the results.

Rescaling a run

curl -X POST http://localhost:8088/api/v1/runs/r-3f8a91/scale \
  -H 'Content-Type: application/json' \
  -d '{"vus": 500, "rampSeconds": 30}'
FieldTypeMeaning
vusintThe new peak virtual user count. The profile's shape is kept and rescaled to this ceiling.
rampSecondsnumberSpread the change over this period. Zero is immediate.

Both must be non-negative; a negative value is a 400. Prefer a ramp — a step change in concurrency measures the step rather than the service.

The snapshot

GET /api/v1/snapshot returns the coordinator's whole state, and it is the same object --out results.json writes.

{
  "build": { ... },
  "run": { ... },
  "runs": [ ... ],
  "agents": [ ... ],
  "ticks": [ ... ],
  "series": [ ... ],
  "totals": { ... },
  "endpoints": [ ... ],
  "failures": [ ... ],
  "events": [ ... ],
  "resolutionSeconds": 1
}

Use totals, not series. series has one entry per label combination; totals has one correctly merged aggregate per metric. Folding series yourself produces plausible-looking numbers that disagree with the thresholds, because means need re-weighting by count and percentiles need the distributions. Same for endpoints: those percentiles are recomputed from each endpoint's merged distribution across all of its status codes.

The run summary

Under run, and in each element of runs:

FieldTypeMeaning
idstringRun identifier.
namestringFrom the configuration's name.
phasestringWhere the run is in its lifecycle.
createdAt, startAt, startedAt, endedAttimestampstartAt is the agreed start instant, set a little in the future so every node has its orders before the clock starts.
elapsedSecondsnumberTime since the run started.
peakVUsintThe profile's peak.
profilestringHuman-readable description of the load shape.
baseURLstringWhat was being tested.
stopReasonstringPresent when a run was stopped rather than completing.
failurestringPresent when the run itself failed.
thresholdsBreachedboolThe verdict.
tagsmapThe run's tags.
participantsarrayPer-agent quota, shard index and phase.
thresholdsarrayEvery assertion with its result.
statsobjectWhole-run aggregates.

An agent

Elements of agents, and the body of GET /api/v1/agents:

FieldTypeMeaning
id, hostname, versionstringThe agent's identity, as advertised when it joined.
cores, maxWorkers, maxVUsintAdvertised capacity.
labelsmapOperator-assigned, for grouping in the dashboard.
remoteAddrstringWhere the connection is coming from.
joinedAt, lastSeentimestamp
activeVUsintSummed across this agent's workers.
healthyWorkersintHow many worker processes are currently connected.
healthyboolWhether its heartbeat is current.
vuQuotaintThis agent's share of the active run's peak, if any.
cpuPercent, memBytesnumber, intThe agent process's own footprint — supervision, not the load its workers generate.
workersarrayPer-process breakdown, below.

A worker

Elements of an agent's workers:

FieldTypeMeaning
idstringThe worker's node id, e.g. local-w0-1.
indexintIts position among this agent's spawned workers.
activeVUsintVirtual users this process is currently running.
cpuPercentnumberCPU time as a percentage of one core, averaged over the process's life so far — 100 means one core fully busy, sustained.
memBytesintResident memory.

cpuPercent and memBytes come from the OS, not from Go's own runtime statistics, which describe the heap rather than the process. A worker measures itself; an agent reports its own footprint the same way and relays each worker's, so both figures are always real rather than approximated from garbage-collector internals.

A threshold result

FieldTypeMeaning
metric, stat, op, valueThe assertion as written.
actualnumberWhat was measured.
evaluatedboolFalse means the metric was never produced. Not the same as a pass.
passedboolWhether it held.
abortOnFailboolWhether a breach stops the run.
descriptionstringRendered form, as printed in the report.

A tick

Elements of ticks, one per resolutionSeconds of the run — this is what the live charts plot:

FieldMeaning
tBucket start, epoch milliseconds, aligned to the wall clock.
vus, requests, failures, iterationsCounts within the bucket.
rps, errorRateDerived rates.
avg, p50, p90, p95, p99Response time. The percentiles are computed from the merged histogram before it is released, so they are correct.
statusMap of status code to count.
scenariosPer-scenario breakdown of the same figures.
endpointsPer-endpoint avg, requests and errorRate. Average only — see retention.

Fetching a report

curl -O http://localhost:8088/api/v1/runs/r-3f8a91/report.html

Served as a download with a generated filename, rather than as a page: this is an artefact to keep, attach to a ticket or archive alongside a release, not another view of the live dashboard. It is one self-contained HTML file with the charts as inline SVG.

A 409 means the run has no results to report yet.

The live stream

GET /api/v1/stream        (WebSocket)

One envelope with a discriminating type, so a client has a single message handler rather than a connection state machine.

The first frame is always {"type": "snapshot", "snapshot": {...}} — a client connecting mid-run sees the history it missed rather than starting its charts from wherever the next tick happens to land. After that come incremental updates carrying whichever of run, agents, ticks, thresholds and events changed.

The connection is pinged every 25 seconds so intermediaries do not close it during the quiet stretch between runs.

The origin check is strict. Reaching the dashboard through a reverse proxy on a different hostname needs that origin allowed explicitly:

loadwave serve --allowed-origin https://loadwave.internal.example.com

Shutting the process down

curl -X POST http://localhost:8088/api/v1/shutdown

Only available when the server was started with --allow-shutdown; this is also what puts the Power off control in the dashboard.

Stopping a run and stopping the process are deliberately different actions — a run ending leaves the coordinator serving, still holding its results.

See also

On this page