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
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/health | Liveness. |
GET | /api/v1/version | Version and build information. |
GET | /api/v1/snapshot | The coordinator's whole current state. |
GET | /api/v1/agents | Connected agents. |
GET | /api/v1/runs | Known runs, newest first. |
POST | /api/v1/runs | Start a run from a configuration. |
GET | /api/v1/runs/{id} | One run's full state. |
GET | /api/v1/runs/{id}/config | The exact configuration the run was started from. |
PUT | /api/v1/runs/{id}/config | Save an edit back to the file the run was started from. |
POST | /api/v1/runs/{id}/stop | End a run. |
POST | /api/v1/runs/{id}/scale | Change a running test's peak VU count. |
GET | /api/v1/runs/{id}/series | Cumulative per-series aggregates. |
GET | /api/v1/runs/{id}/report.html | The self-contained HTML report, as a download. |
POST | /api/v1/shutdown | Stop the process. Only when started with --allow-shutdown. |
GET | /api/v1/stream | WebSocket: 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/runsThe 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" }| Status | Meaning |
|---|---|
202 | Accepted; the run is starting. |
400 | The body was empty or the configuration did not parse. The message is the parser's. |
409 | The start was rejected — no agents, or a run already in progress. |
403 | The 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/config200 OK
{
"yaml": "name: checkout\nbaseURL: https://staging.example.com\n...",
"sourcePath": "/home/ci/test.yaml",
"draft": { "name": "checkout", "baseURL": "https://staging.example.com", "...": "..." }
}| Field | Type | Meaning |
|---|---|---|
yaml | string | The exact configuration the run was started from. |
sourcePath | string | The 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. |
draft | object | The 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| Status | Meaning |
|---|---|
200 | Saved. The response body is {"savedTo": "<path>"}. |
400 | The body did not parse as a configuration. Nothing was written. |
404 | No such run. |
409 | This run has no source file — there is nothing to save to. |
403 | The 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"}'| Field | Type | Default | Meaning |
|---|---|---|---|
graceful | bool | true | Let in-flight iterations finish. |
reason | string | — | Recorded 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}'| Field | Type | Meaning |
|---|---|---|
vus | int | The new peak virtual user count. The profile's shape is kept and rescaled to this ceiling. |
rampSeconds | number | Spread 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:
| Field | Type | Meaning |
|---|---|---|
id | string | Run identifier. |
name | string | From the configuration's name. |
phase | string | Where the run is in its lifecycle. |
createdAt, startAt, startedAt, endedAt | timestamp | startAt is the agreed start instant, set a little in the future so every node has its orders before the clock starts. |
elapsedSeconds | number | Time since the run started. |
peakVUs | int | The profile's peak. |
profile | string | Human-readable description of the load shape. |
baseURL | string | What was being tested. |
stopReason | string | Present when a run was stopped rather than completing. |
failure | string | Present when the run itself failed. |
thresholdsBreached | bool | The verdict. |
tags | map | The run's tags. |
participants | array | Per-agent quota, shard index and phase. |
thresholds | array | Every assertion with its result. |
stats | object | Whole-run aggregates. |
An agent
Elements of agents, and the body of GET /api/v1/agents:
| Field | Type | Meaning |
|---|---|---|
id, hostname, version | string | The agent's identity, as advertised when it joined. |
cores, maxWorkers, maxVUs | int | Advertised capacity. |
labels | map | Operator-assigned, for grouping in the dashboard. |
remoteAddr | string | Where the connection is coming from. |
joinedAt, lastSeen | timestamp | — |
activeVUs | int | Summed across this agent's workers. |
healthyWorkers | int | How many worker processes are currently connected. |
healthy | bool | Whether its heartbeat is current. |
vuQuota | int | This agent's share of the active run's peak, if any. |
cpuPercent, memBytes | number, int | The agent process's own footprint — supervision, not the load its workers generate. |
workers | array | Per-process breakdown, below. |
A worker
Elements of an agent's workers:
| Field | Type | Meaning |
|---|---|---|
id | string | The worker's node id, e.g. local-w0-1. |
index | int | Its position among this agent's spawned workers. |
activeVUs | int | Virtual users this process is currently running. |
cpuPercent | number | CPU time as a percentage of one core, averaged over the process's life so far — 100 means one core fully busy, sustained. |
memBytes | int | Resident 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
| Field | Type | Meaning |
|---|---|---|
metric, stat, op, value | — | The assertion as written. |
actual | number | What was measured. |
evaluated | bool | False means the metric was never produced. Not the same as a pass. |
passed | bool | Whether it held. |
abortOnFail | bool | Whether a breach stops the run. |
description | string | Rendered form, as printed in the report. |
A tick
Elements of ticks, one per resolutionSeconds of the run — this is what the live
charts plot:
| Field | Meaning |
|---|---|
t | Bucket start, epoch milliseconds, aligned to the wall clock. |
vus, requests, failures, iterations | Counts within the bucket. |
rps, errorRate | Derived rates. |
avg, p50, p90, p95, p99 | Response time. The percentiles are computed from the merged histogram before it is released, so they are correct. |
status | Map of status code to count. |
scenarios | Per-scenario breakdown of the same figures. |
endpoints | Per-endpoint avg, requests and errorRate. Average only — see retention. |
Fetching a report
curl -O http://localhost:8088/api/v1/runs/r-3f8a91/report.htmlServed 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.comShutting the process down
curl -X POST http://localhost:8088/api/v1/shutdownOnly 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.