# Load test suite (k6)
This directory contains the production-rehearsal k6 load tests for the
boilerplate. They are the load-tier companion to the unit, integration, and
verifier layers — useful before a release cut, after a perf regression
suspicion, or when sizing a new staging tier.
The suite is **opt-in**. CI does not run it by default because it requires a
deployed target environment, a tenant API key, and a k6 binary on the
runner. `scripts/run-load-tests.mjs` is the safe wrapper: it SKIPs cleanly
when either prerequisite is missing.
## Install k6
k6 is a single Go binary — install it from the upstream packages:
- macOS: `brew install k6`
- Linux: see
- Container: `grafana/k6:latest` (mount this repo and run inside)
Verify the install with `k6 version`. The runner script checks the same.
## Scripts
| Script | Profile | Thresholds |
| --- | --- | --- |
| `login.js` | 50 VUs, 30s ramp + 30s hold against `POST /v1/auth/login` | `http_req_failed: rate<0.05`, `http_req_duration: p(99)<800ms`, `http_req_duration{name:login}: p(95)<400ms` |
| `api-read.js` | 100 VUs, 60s constant against `GET /v1/entities//rows` | `http_req_duration: p(99)<300ms`, `http_reqs: rate>200/s` |
| `api-mutating.js` | 30 VUs, 30s constant against `POST /v1/entities//rows` | `http_req_duration: p(99)<600ms` |
| `scenarios.js` | Composite — runs all three back-to-back | Per-suite thresholds tagged by `name:` |
Each per-script file is also runnable standalone — useful when isolating a
regression to one path.
## Threshold rationale
- `http_req_failed: rate<0.05` on login allows for 401s from the synthetic
account while still catching server-side 5xx storms.
- `http_req_duration: p(99)<800ms` on login covers password hashing + session
creation under realistic contention. Tighten as the auth path matures.
- `http_req_duration{name:login}: p(95)<400ms` keeps the typical login fast;
it is the user-facing budget, not the worst-case.
- `http_req_duration: p(99)<300ms` on the read path matches the
warm-cache-hit budget for the hottest API surface.
- `http_reqs: rate>200/s` is a throughput floor — percentile checks alone
miss regressions that quietly halve RPS under contention.
- `http_req_duration: p(99)<600ms` on the mutating path covers transactional
write + audit + queue enqueue.
If a threshold becomes too tight for a particular environment (e.g. a small
staging tier), override at invocation time with `k6 run --threshold` rather
than editing the script — keep the production budget in source.
## Running locally
```sh
# Single script
K6_TARGET_URL=http://localhost:3000 \
LOAD_TENANT_API_KEY=tnk_load_test_key \
k6 run tests/load/login.js
# Full suite via the safe runner (SKIPs if k6 is missing or target down)
K6_TARGET_URL=http://localhost:3000 \
LOAD_TENANT_API_KEY=tnk_load_test_key \
pnpm run verify:load
```
Required env vars:
- `K6_TARGET_URL` — base URL of the target environment (default
`http://localhost:3000`).
- `LOAD_TENANT_API_KEY` — tenant-scoped API key for the test tenant. The
read/mutating scripts default to a placeholder that will return 401 — fine
for SKIP-rehearsal but not for a real load run.
Optional env vars:
- `LOAD_LOGIN_EMAIL` / `LOAD_LOGIN_PASSWORD` — synthetic login credentials.
- `LOAD_ENTITY_SLUG` — entity slug to target on the read/mutating paths.
## Stress vs. smoke
To scale a smoke run down, pass `--vus 5 --duration 10s` to `k6 run`. To
scale a stress run up, increase the per-script VUs via the k6 CLI rather
than editing the file. The committed VU counts are the production-rehearsal
contract.
## Safety notes
- Never point a load test at production without an explicit human approval.
- Mutating runs append synthetic rows tagged with `load-test-row-*` so they
can be cleaned up by a downstream verifier. Pick a throwaway tenant.
- Load tests are part of the production-readiness ladder, not part of CI
Auto-Mode. Keep `verify:load` out of the default lane until a target
environment is wired up.