Using k6 Thresholds as Performance Gates in CI/CD
Turn k6 load-test metrics into CI/CD pass/fail gates using Thresholds on aggregated metrics like p(95) latency and error rate.
17 Mar 2026, 21:22 UTC

The problem: green pipelines for slow APIs
You add a k6 load test to your CI pipeline. The virtual users finish, the terminal prints a summary, and the build turns green. But the 95th percentile response time is 800 ms, well above your service-level objective. Without a pass/fail rule, k6 reports metrics but does not fail the build, so performance regressions ship unnoticed.
The useful takeaway is that k6 Thresholds let you define explicit pass/fail criteria on aggregated metrics. When a threshold is breached, k6 exits with a non-zero status code, which CI/CD systems can treat as a hard gate.
What thresholds evaluate
Thresholds operate on aggregated results after the test finishes. They are not per-request triggers and they cannot stop a test mid-execution on their own. You define them in the options.thresholds object using metric names and expressions such as p(95)<500 or rate<0.01.
Common metrics include http_req_duration for response time and http_req_failed for failed requests. A threshold can apply globally to every request or be targeted at a specific request group using tags.
Worked example: a latency gate
This script defines two thresholds: the 95th percentile of request duration must stay under 500 ms, and the failed request rate must stay under 1 percent.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
thresholds: {
'http_req_duration': ['p(95)<500'],
'http_req_failed': ['rate<0.01'],
},
vus: 20,
duration: '30s',
};
export default function () {
const res = http.get('http://localhost:8080/api/items');
check(res, { 'status is 200': (r) => r.status === 200 });
}
Run the script from a terminal on your local machine or CI runner. Replace http://localhost:8080/api/items with your actual endpoint.
k6 run perf.js
echo $?
Where to run: any host with the k6 binary installed and outbound network access to the target. No special permissions are required beyond executing k6 and making HTTP requests.
Expected checks:
- If p(95) stays under 500 ms and the failure rate stays under 1 percent, k6 exits with code 0.
- If either threshold is breached, k6 exits with a non-zero code.
Risk: transient network jitter or one slow response can push p(95) over the limit. Run the script multiple times against a stable environment before using it to block deployments.
Targeting specific endpoints with tags
You can scope a threshold to a tagged request instead of applying it globally. This is useful when different endpoints have different SLOs.
export const options = {
thresholds: {
'http_req_duration{name:GetUsers}': ['p(95)<300'],
'http_req_duration{name:CreateUser}': ['p(95)<800'],
},
};
export default function () {
http.get('http://api.example.com/users', {}, { tags: { name: 'GetUsers' } });
http.post('http://api.example.com/users', JSON.stringify({ name: 'alice' }), { tags: { name: 'CreateUser' } });
}
Caution: avoid high-cardinality tags such as user IDs or request IDs. Each unique tag combination increases memory consumption during the test.
Limitations
Thresholds are evaluated at the end of the test. They do not abort a running test when a metric starts to degrade. If you need mid-test abort behavior, you must add script logic, such as checking error rates inside the default function and calling test.abort().
Defining thresholds too aggressively can produce flaky tests. Treat the first few runs as observations, not gates, and tune the values based on real variance.
Practical check before you merge
- Run the script against a fast, healthy endpoint and confirm
echo $?returns 0. - Run the script against a known slow endpoint or introduce an artificial delay and confirm
echo $?returns non-zero. - Repeat the healthy-endpoint run three to five times to check for flakiness caused by jitter.
Closing
Start with one or two thresholds on your most important endpoint. Verify the exit codes locally, then add the k6 run step to your CI pipeline. Because k6 exits non-zero on a breach, the pipeline can fail the build before a performance regression reaches production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.