Enforcing Performance Budgets with k6 Thresholds in CI/CD Pipelines
Learn how to codify performance budgets as k6 thresholds, wire them into CI/CD for automated regression gates, and handle flakiness and statistical pitfalls.
10 Sept 2026, 06:21 UTC

Desired Outcome
Configure k6 load tests to automatically fail a CI/CD pipeline when response-time percentiles or error-rate targets are missed, turning subjective performance goals into enforceable service-level objectives (SLOs).
Prerequisites
- k6 v0.45+ installed locally and available in the CI runner.
- A working k6 script that exercises the target API or web application.
- A CI/CD system that treats a non-zero exit code as a job failure (GitHub Actions, GitLab CI, Jenkins, CircleCI, etc.).
Define Thresholds in the Test Script
Thresholds live in the options.thresholds object. Each entry maps a metric name to a pass/fail expression. k6 evaluates every expression at the end of the test (or immediately when abortOnThreshold is enabled) and exits with code 99 if any expression evaluates to false.
export const options = {
thresholds: {
'http_req_duration': ['p(95)<200', 'p(99)<500'],
'http_req_failed': ['rate<0.01'],
'checks': ['rate>0.99']
},
abortOnThreshold: true // optional: stop the test the moment a critical threshold is crossed
};
The example above enforces three SLOs:
- 95th-percentile request duration must stay under 200 ms.
- 99th-percentile request duration must stay under 500 ms.
- Failed-request rate must stay below 1 %.
- Custom check success rate must exceed 99 %.
Metric names correspond to built-in k6 metrics (http_req_duration, http_req_failed, checks) or any custom Trend, Rate, or Counter you create.
Absolute vs. Percentage-Based Expressions
Threshold expressions use a compact DSL:
- Absolute latency:
p(95)<200(95th percentile < 200 ms),avg<100,max<1000. - Rate / ratio:
rate<0.01(error rate < 1 %),rate>0.99(success rate > 99 %). - Count:
count>1000(at least 1,000 samples).
Multiple expressions for the same metric are AND-ed together; all must pass.
Integrate with CI/CD
Because k6 exits with 99 on threshold failure, no extra parsing is required. A minimal GitHub Actions step looks like:
- name: Run k6 performance test
run: k6 run --quiet script.js
If any threshold is breached, the step fails and the workflow stops. The --quiet flag suppresses the progress bar while keeping the summary and threshold verdict.
Using abortOnThreshold for Fast Feedback
Set abortOnThreshold: true (or pass --abort-on-threshold on the CLI) when a single catastrophic regression—such as a 500-error spike—should halt the test immediately. This saves CI minutes and prevents noisy downstream metrics. Note that abortOnThreshold evaluates thresholds continuously during the run, so transient spikes can still trigger an abort; pair it with a short gracePeriod (e.g., thresholds: { 'http_req_failed': [{threshold: 'rate<0.01', abortOnFail: true, delay: '10s'}] }) to ignore warm-up jitter.
Expected Checks After a Run
- CLI summary: Verify the
Thresholdssection shows✓for every expression. - Exit code:
echo $?should be0on success,99on threshold failure, other non-zero codes for script errors. - Artifact upload (optional): Save the JSON output (
--out json=result.json) for historical trend dashboards.
Common Failure Modes and Recovery
Flaky pipelines due to network jitter
Overly aggressive absolute thresholds (e.g., p(99)<50 on a cross-region call) cause false positives. Remediation:
- Widen the budget or move the threshold to a lower percentile (
p(95)). - Add a
delayto the threshold so it is only evaluated after a warm-up period. - Run the test against a staging environment in the same network zone as production.
Statistically insignificant high percentiles
A p(99.9) threshold with only 200 requests is mathematically unstable. Remediation:
- Increase virtual users or test duration to collect ≥ 1,000 samples for p99, ≥ 10,000 for p99.9.
- Prefer
p(95)orp(99)for routine gates; reserve p99.9 for dedicated soak tests.
Thresholds evaluated only at test end
Without abortOnThreshold, a 30-minute soak test that breaches an error-rate target in minute 5 still runs to completion. Remediation:
- Enable
abortOnThresholdfor critical metrics (error rate, check success rate). - Keep latency thresholds evaluated at the end to avoid aborting on a single GC pause.
Concrete Example: Regression Gate for a REST API
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '30s', target: 0 }
],
thresholds: {
'http_req_duration': ['p(95)<250', 'p(99)<600'],
'http_req_failed': ['rate<0.005'],
'checks': ['rate>0.995']
},
abortOnThreshold: true
};
export default function () {
const res = http.get('https://api.example.com/orders/123');
check(res, {
'status is 200': (r) => r.status === 200,
'response has orderId': (r) => r.json('orderId') !== ''
});
sleep(1);
}
Running k6 run script.js locally produces a summary ending with:
Thresholds
http_req_duration
✓ p(95)<250
✓ p(99)<600
http_req_failed
✓ rate<0.005
checks
✓ rate>0.995
If the 99th percentile drifts to 620 ms, the run aborts at the next evaluation interval, exits 99, and the CI job fails.
Limitations
- Thresholds cannot reference external systems (e.g., Datadog SLOs) directly; export JSON and post-process if needed.
- No built-in hysteresis: a metric that oscillates around the boundary will flip the pass/fail state each run.
- High-percentile accuracy depends on sample size; k6 does not emit confidence intervals.
Verification Checklist
- Run the script with an impossible threshold (
p(95)<1) and confirm exit code 99. - Inject a 5 % error rate (e.g., point to a 404 endpoint) and verify
http_req_failedthreshold fails. - Compare the
Thresholdsblock in the CLI output against theoptions.thresholdsobject to ensure every expression is tracked.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.