Enforce k6 load tests with thresholds instead of guessing
k6 thresholds turn load test output into automated pass/fail gates. Learn how to scope them by tag, use abortOnFail, and verify exit codes in CI.
07 Oct 2025, 16:07 UTC

A clean k6 run is not the same as a passing test
You run a k6 load test, watch the terminal scroll past request lines, and see no obvious errors. You close the window and assume the system held up. But k6 prints request-level detail by default; it does not fail the run just because response times doubled or a small percentage of requests errored. Without explicit pass/fail criteria, a load test is observation, not a gate.
The useful takeaway is simple: define thresholds in your script so k6 decides pass or fail, and let CI enforce the exit code.
What a threshold actually does
A threshold is a pass/fail expression attached to a metric in the script’s options object. k6 evaluates each expression after the test finishes. If any expression is false, k6 reports a threshold failure and exits with a non-zero code, which CI/CD pipelines can treat as a broken build.
Common built-in metrics include http_req_duration, http_req_failed, iterations, and vu_users. You can also track custom metrics with the k6/metrics Metric API.
Example declarations:
http_req_duration: ['avg<200']— average request duration under 200 mschecks: ['rate==1']— every check must passhttp_req_failed: ['rate<0.01']— fewer than 1 percent of requests fail
Scope thresholds so they do not fail unrelated traffic
An unscoped threshold applies to every matching metric across the entire test. If you have a login endpoint and a search endpoint in the same script, a broad http_req_duration limit can fail the run because of slow search traffic, even when login is healthy.
Scope by tags instead. Tag scoping uses the tags k6 attaches to metrics, such as HTTP status code or custom tags you add to requests:
thresholds: {
'http_req_duration{status:200}': ['avg<200'],
'http_req_duration{status:200,endpoint:search}': ['p(95)<500'],
}
You can combine multiple expressions in an array for stricter logic.
Worked example: fail fast on search API regressions
This script loads two endpoints and applies a stricter latency threshold only to successful search requests. It also enables abortOnFail, which tells k6 to stop the test early when a threshold breaches, avoiding wasted load generation.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 20,
duration: '30s',
thresholds: {
'http_req_duration{status:200}': ['avg<200'],
'http_req_duration{status:200,endpoint:search}': ['p(95)<500'],
checks: ['rate==1'],
},
abortOnFail: true,
};
export default function () {
const login = http.get('https://test-api.example.com/login', {
tags: { endpoint: 'login' },
});
check(login, { 'login status 200': (r) => r.status === 200 });
const search = http.get('https://test-api.example.com/search', {
tags: { endpoint: 'search' },
});
check(search, { 'search status 200': (r) => r.status === 200 });
}
Run the script from a terminal where k6 is installed:
k6 run load-test.js
No special permissions are required for a local run. Replace load-test.js with your script path. After the run, check the exit code:
echo $?
A zero exit means all thresholds passed. A non-zero exit means at least one threshold failed. With abortOnFail: true, k6 may stop before the full duration elapses, so you lose later-stage data such as ramp-down behavior. Use it when failing fast matters more than collecting a complete curve.
Limitations and how to verify
Thresholds evaluate final aggregates, so one slow request will not fail the test by itself. Percentile thresholds like p(95)<500 can be unstable with low iteration counts and may produce flaky results in short tests.
To verify your setup, deliberately set an impossible threshold, such as avg<50 against a local mock server with known higher latency, then run:
k6 run load-test.js; echo $?
Confirm that k6 prints a threshold failure and returns a non-zero exit code. Then relax the expression and confirm the run passes. You can also run k6 version to confirm your installed release supports the threshold syntax you are using.
Closing
Start with one threshold on your most important endpoint, confirm the exit code locally, then add the script to your pipeline. Absence of red text in the terminal is not a success criterion; a failing exit code is.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.