4.0 KiB
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 https://k6.io/docs/getting-started/installation/
- 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/<slug>/rows |
http_req_duration: p(99)<300ms, http_reqs: rate>200/s |
api-mutating.js |
30 VUs, 30s constant against POST /v1/entities/<slug>/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.05on login allows for 401s from the synthetic account while still catching server-side 5xx storms.http_req_duration: p(99)<800mson login covers password hashing + session creation under realistic contention. Tighten as the auth path matures.http_req_duration{name:login}: p(95)<400mskeeps the typical login fast; it is the user-facing budget, not the worst-case.http_req_duration: p(99)<300mson the read path matches the warm-cache-hit budget for the hottest API surface.http_reqs: rate>200/sis a throughput floor — percentile checks alone miss regressions that quietly halve RPS under contention.http_req_duration: p(99)<600mson 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
# 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 (defaulthttp://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:loadout of the default lane until a target environment is wired up.