Using k6 thresholds as CI pass/fail gates
Use k6 thresholds as CI pass/fail gates for load tests. Define latency and check criteria, fail builds on non-zero exit, and sanitize artifacts.
07 Sept 2026, 05:34 UTC

The problem: silent performance regressions
A load test can finish successfully while the system under test is slower than before. By default, k6 exits zero when the test completes, regardless of response times. CI pipelines that only check the exit code therefore merge performance regressions unnoticed.
Useful takeaway
Define thresholds in the k6 script and treat a non-zero exit code as a build failure. Thresholds aggregate metrics after the run ends, so they work as a post-test gate, not as a mid-test circuit breaker.
Requirements
- A CI runner that can execute the k6 binary.
- Explicit permission to send load to the target system. High request rates can resemble a denial-of-service attack to third parties.
- A way to inject secrets at runtime, such as environment variables or a secret manager.
Smallest suitable design
One default scenario, one latency threshold, one status check, and JSON output for CI artifacts. Virtual users (VUs) are simulated execution threads that run the script iterations.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'],
checks: ['rate>0.99'],
},
scenarios: {
default: {
executor: 'constant-vus',
vus: 10,
duration: '30s',
},
},
};
export default function () {
const res = http.get(__ENV.TARGET_URL);
check(res, {
'status is 200': (r) => r.status === 200,
});
}
`p(95)<500` means the 95th percentile of request duration must be under 500 milliseconds. `checks` is the aggregated pass rate of all `check()` calls in the script. A check is a per-iteration boolean assertion written inside VU code.
Running the gate locally
Run from a workstation or CI runner with a user account that can execute k6 and open outbound network connections to the target.
k6 run --version
export TARGET_URL=https://api.example.com/health
k6 run script.js --out json=results.json
Expected result: if the 95th percentile exceeds 500 ms or the check rate falls below 99 percent, k6 exits with a non-zero status code. Compare the terminal threshold summary with the `metrics` block in `results.json` to confirm they agree.
Risks: the script sends live traffic to `__ENV.TARGET_URL`. Do not point it at endpoints you do not own or have not been authorized to test. JSON output can include response headers and bodies, so sanitize `results.json` before archiving it in a shared repository if it might contain personal data, tokens, or internal identifiers.
Trust and data boundaries
- Script source: never embed API tokens, passwords, or connection strings. Pass them through `__ENV` or secret injection at runtime. k6 executes with the privileges of the k6 process.
- Target boundary: limit traffic to systems you own or are explicitly authorized to load-test.
- Artifact boundary: treat k6 output as potentially sensitive until reviewed.
Operational checks
- Verify the installed release: `k6 run --version`. Threshold behavior can differ between releases, so confirm the version before relying on exit codes.
- Run a short ramp with a strict threshold and confirm the process exits non-zero when the threshold is breached.
- Monitor CPU and memory on the generator machine during the ramp. If the runner saturates before the target does, the results measure the load generator, not the system under test.
- Compare terminal output with JSON artifacts in CI to ensure pass/fail status is consistent.
Failure modes
- Late failure: thresholds evaluate after test completion. A 10-minute test consumes generator resources for the full duration before CI learns the gate failed.
- No guaranteed early abort: do not assume k6 stops the test the moment a threshold is breached. Verify the installed release if early abort is required for resource-constrained runners.
- Generator saturation: adding VUs on a small runner can inflate latency locally and produce false failures.
Conditions that would change the design
- Local VU capacity saturates: move to distributed execution or a managed k6 service.
- Cross-region load is required: a single runner cannot represent geographic traffic; use multiple launch points.
- Result retention and collaboration exceed local file-based reporting: integrate with a time-series dashboard or k6 cloud instead of archiving JSON files.
- The test writes state rather than only reading: then a failed run may leave the target in a modified state, and rollback becomes a separate operational concern.
Limitation and verification
This design catches regressions after the test ends. It does not protect a long-running test from consuming budget before the gate trips. Verify locally by breaching a threshold on purpose and observing the non-zero exit code before enabling the gate in CI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.