From Eyeballing k6 Graphs to Automated Quality Gates: Encoding SLOs as Thresholds and Checks
k6 can generate rich metrics, but without thresholds and checks your CI pipeline has no verdict. Learn to encode latency SLOs and correctness assertions as executable pass/fail criteria.
09 May 2026, 04:32 UTC

Many k6 scripts generate rich metrics but no verdict. The dashboard looks impressive, the JSON export is thorough, and yet the build stays green because nobody defined what “passing” actually means. The useful takeaway: encode your service-level objectives as k6 thresholds and checks, and let the exit code decide.
Separating “It Responded” from “It Met the Goal”
k6 gives you two distinct mechanisms for pass/fail logic, and they answer different questions. Checks are per-request assertions—does this response contain a 200 status, is the JSON payload well-formed, is the error message absent? They produce a checks metric with a pass rate, but by default they do not fail the run. Thresholds are conditions on aggregated metrics evaluated at the end of the test—for example, http_req_duration p95 under 500 ms, or http_req_failed rate below 1%. When a threshold is violated, k6 exits with a non-zero status code, which is exactly what CI needs.
To turn a correctness check into a gate, add a threshold on the checks metric itself, such as checks: ['rate>0.99']. This separates the concern of “the system responded” from “the system met its goal,” and it keeps your assertions visible in the end-of-test summary.
Worked Example: A Smoke Gate for a Critical Endpoint
Suppose your checkout service has one critical endpoint: GET /v1/orders. You want to verify that as traffic ramps from zero to a moderate arrival rate, the p95 latency stays under 500 ms and the endpoint keeps returning HTTP 200. The following script uses a ramping-arrival-rate executor so the SLO is tested under changing load, not just a flat virtual-user count.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
checkout: {
executor: 'ramping-arrival-rate',
startRate: 0,
timeUnit: '1s',
preAllocatedVUs: 10,
maxVUs: 50,
stages: [
{ target: 5, duration: '30s' },
{ target: 10, duration: '1m' },
{ target: 0, duration: '30s' },
],
},
},
thresholds: {
http_req_duration: ['p95<500'],
http_req_failed: ['rate<0.01'],
checks: ['rate>0.99'],
},
};
export default function () {
const res = http.get('https://api.example.com/v1/orders');
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
Run it locally from the directory containing the script with k6 run smoke.js. No special permissions are required beyond network access to the target endpoint. The end-of-test summary will list each threshold and whether it passed. If the p95 exceeds 500 ms, the failed-request rate exceeds 1%, or fewer than 99% of checks pass, k6 returns a non-zero exit code and your CI job fails.
To confirm the gate actually works, temporarily tighten the latency threshold to p95<50 and rerun. The run should now fail with a non-zero exit status. When a threshold fails unexpectedly, capture raw samples with k6 run --out json=results.json smoke.js and inspect the exported metric points offline.
The Trade-off: Aggregated Thresholds Hide Outliers
Thresholds are coarse by design. A p95 under 500 ms can conceal a 5% tail of multi-second responses that still damages user experience. Over-tightening thresholds to catch every outlier creates flaky CI gates that fail on noise, eroding trust in the signal. Calibrate thresholds against observed baselines and business impact: a 500 ms p95 may be fine for an internal admin API and unacceptable for a checkout flow.
Two further limitations matter. First, k6 evaluates thresholds against aggregated metrics at the end of a run by default; the test does not abort mid-run unless the scenario or script explicitly supports it. Second, checks and thresholds only cover what the script exercises. If you omit a critical request, you omit its SLO. Finally, a non-zero exit code tells you that something failed, but diagnosing why requires reading the end-of-test summary or the exported metrics.
Start with One Journey, One Gate
Do not boil the ocean. Pick a single critical user journey, add one latency threshold and one correctness check, and run it locally until the summary reads clean. Promote that script to CI as a smoke gate. Once the team trusts the signal, extend coverage to additional endpoints, tighten thresholds as baselines stabilize, and layer on more complex scenarios. The goal is not a perfect load test on day one; it is a build that fails automatically when the system stops meeting its goals.