Load Testing
Testing reuses your functional test definitions as the load workload: the same steps you run
with test.svc tests are executed repeatedly by concurrent virtual users, phase by phase. There is
no separate load-script format — you write a load profile describing phases and point it at
an existing test collection.
Two pipelines can drive load today:
| Pipeline | Command | Status |
|---|---|---|
| tests-mode load (recommended) | test.svc tests -p <dir> --load <profile.yaml> --profile <name> | current; renders phase summary tables |
| legacy load | test.svc load -i test.yaml -c config.yaml -r load.yaml -l <name> | v1 testplan format; live terminal dashboard |
The v2 kis test runner is functional-only for now — its DuckDB schema notes that load
metrics tables ship when those executors land.
Quick start
Section titled “Quick start”test.svc tests -p examples/06-load-test -d examples/env.json \ --load examples/06-load-test/profile.yaml --profile default -sWorkload steps are ordinary step definitions (examples/06-load-test/steps.yaml):
steps: - name: load-get-users rest: url: "{{baseurl}}/users" method: GET
- name: load-create-post rest: url: "{{baseurl}}/posts" method: POST body: type: raw payload: | { "title": "Load test post", "body": "Created during load testing", "userId": 1 } headers: - Content-Type: application/jsonUse --patterntype/--pattern to narrow which tests form the workload; each virtual user
executes one full pass over the filtered set.
Load profiles
Section titled “Load profiles”A profile file has a top-level loadprofiles: key with named profiles. Select one with
--profile <name> (default default).
loadprofiles: default: # gentle profile for smoke testing warmup: duration: 10s targetusers: 5 sustained: duration: 30s targetusers: 10 cooldown: duration: 10s targetusers: 2
stress: warmup: duration: 15s targetusers: 20 sustained: duration: 60s targetusers: 50 cooldown: duration: 15s targetusers: 5
burst: # phases are optional — one is enough sustained: duration: 10s targetusers: 100Phases
Section titled “Phases”Exactly three phase slots exist, always executed in this order, each optional:
warmup → sustained → cooldownA missing phase is skipped with a log line (“no warmup phase found. skipping…”).
Each phase is a constant-concurrency plateau. There is no ramp, spike, or pause phase type:
targetusers is a cap that the spawner fills as fast as admission allows, not a gradient.
“Ramping” is approximated by chaining phases with increasing caps.
Per-phase fields
Section titled “Per-phase fields”| Field | Type | Required | Meaning |
|---|---|---|---|
duration | Go duration string (10s, 1m) | yes — parse failure aborts | how long the phase spawns users |
targetusers | int | one of the two | max concurrent virtual users (takes priority when both set) |
targetrequests | int | one of the two | max concurrent in-flight requests |
delay.duration | duration string, default 1ms | no | pacing sleep between user spawns (legacy test.svc load pipeline only) |
Profile-level fields (legacy test.svc load pipeline only)
Section titled “Profile-level fields (legacy test.svc load pipeline only)”| Field | Default | Meaning |
|---|---|---|
cpu | runtime.NumCPU() | GOMAXPROCS cap for the run |
phasedelay | 0s | sleep between phases |
Total run duration = sum of phase durations (there is no overall duration cap).
Where the profile file lives
Section titled “Where the profile file lives”test.svc tests:--load <file>+--profile <name>.test.svc load:-r/--profiles <file>(defaultload.yaml); if you pass-r ""the profiles are read from aloadprofiles:key inside the-iinput file itself.
Execution model
Section titled “Execution model”A virtual user is a goroutine that executes one full pass of the workload (every filtered test, all steps, all data records), then releases its slot.
- A 1 ms ticker loop runs for the phase
duration. Each tick it computes how many users are missing to reachtargetusersand spawns that many, each gated through the governor’s blocking admission check. - When a user finishes its pass, the ticker replenishes it — so short workloads recycle users rapidly; long workloads may not reach the cap before the phase ends.
- The governor is a concurrency semaphore on atomics — it tracks current/total users and
requests. There is no arrival-rate model: throughput is an emergent property of
targetusers, workload latency, and (legacy pipeline)delay.durationspawn pacing. - Each individual HTTP call additionally acquires a request slot when
targetrequestsgates the phase. - Phase end stops the spawner only — in-flight users finish their pass naturally; nothing force-kills them.
Differences between pipelines:
- Legacy
test.svc loadpaces spawns withdelay.duration; tests-mode does not. - In load mode the functional per-step table output is suppressed, and interactive
userinputprompts are skipped.
Caveat: the HTTP client currently applies no request timeout in load mode (the env
timeoutkey is read but not wired to the client). A hung endpoint holds its virtual user until the process ends.
Metrics
Section titled “Metrics”Per request, Testing records: duration (ns), TTFB (via go-httpstat), bytes in/out, status
(passed/failed), and the HTTP status code. Aggregated per phase and per run:
- Latency (execution time): min, avg, median, max, and nearest-rank percentiles 50/90/95/99
- TTFB: same set
- Counts: total / passed / failed requests, total users
- Bytes: total in / out
Notes on interpretation:
- Averages are computed over passed records only.
- Percentiles use nearest-rank over the raw sample arrays, aggregated at run level.
- The
TimeoutandNetwork Failurecolumns in the summary table are currently always 0 (the counters are never incremented);Test Step Failuretherefore equals the failed count. - A failed validation marks the record
failed; a regex/JSON-key mismatch that produces no hard error can still count assuccessin the legacy pipeline — assert explicitly on status codes for reliable failure counts.
Live output
Section titled “Live output”- tests-mode: a status line every 100 ms —
Status: Requests [current, total, max] | Users [current, total, max]— then per-phase summary tables (<phase> Phase Results:). When onlytargetusersis set (notargetrequests), the Requests counter is not admission-tracked and drifts negative — read the Users column live and take request totals from the phase table’sTotal Count. Expect verbose debug logging on stdout during load runs; the phase tables print at the end of each phase. - legacy
test.svc load: a termui full-screen dashboard (gauge, status, log window) plus a run summary (Run ID, start/end time, elapsed seconds).
Phase summary table
Section titled “Phase summary table”Columns per step (rendered per phase): Success %, Total Count, Success Count, failure
counts (Total, Timeout, Test Step Failure, Network Failure), Execution Time
(Min/Avg/Median/Max/50/90/95/99), TTFB (same 8), Bytes In, Bytes Out.
The Execution Time and TTFB columns are nanoseconds — divide by 1e6 for milliseconds
(e.g. 119351861 ≈ 119 ms).
| Pipeline | Files |
|---|---|
| tests-mode | <prefix>-<timestamp>.log (from --prefix, default test), phase tables to stdout |
| legacy | <env output prefix>test-load-<timestamp>.log, <prefix>_summary.txt (currently created but left empty — the table-writing call is disabled) |
Pass/fail semantics
Section titled “Pass/fail semantics”There are no configurable thresholds, SLOs, or error-rate gates. A load run “passes” by
completing: the exit code is non-zero only on setup/phase errors, never on failed requests.
Failed requests only show up in the metrics. If you need a CI gate, parse the phase summary
(or wrap the run and inspect the failure counts) — or run a functional pass with kis test
before/after the load run.
Per-record pass/fail comes from the same validation rules as functional runs
(assertions and variables): response.validate expressions
(in the step’s language: js/jq/celgo), else response.regex, else payload comparison.
continueonerror controls whether a failure aborts the user’s pass.
Distributed load
Section titled “Distributed load”For horizontal scale, run test.svc orchestrator plus one or more test.svc agent processes and submit
runs over the orchestrator’s HTTP API — the same profiles and test definitions apply, with
agentcount fanning scenarios out across agents. See server modes.
Sizing guidance
Section titled “Sizing guidance”targetusersbounds concurrency, not RPS. Estimate RPS ≈ users ÷ mean pass latency × requests-per-pass, then validate against the live status line.- Start with the
default-style profile (small warmup, modest sustained) and scaletargetusersbetween runs; the cooldown phase mainly demonstrates the smaller-cap behavior (users above the cap finish naturally, they are not culled). - One process generates load from one machine; use distributed mode when a single generator saturates before the target does.