A k6 check is an assertion on a response, such as “status is 200” or “body contains the user id”, that records pass or fail and lets the test carry on. Checks never fail a k6 run on their own. To make failed checks fail the test, and set a non-zero exit code for CI, you add a threshold on the checks metric.
We tested how checks behave in k6 v2.3.0. A run where two thirds of all checks failed exited with code 0. A threshold on the checks of one endpoint failed the run while the overall check rate still looked healthy. And a check whose code threw an error stopped the whole iteration, skipping the checks after it and every request that followed.
Key Takeaways
check()evaluates each condition, counts it as passed or failed, and returnstrueonly when all of them passed. The test keeps running either way.- Failed checks alone leave the exit code at 0. Add a threshold such as
checks: ['rate>0.99']to make them fail the run with exit code 99. - Thresholds can target one check by name,
checks{check:status is 200}, or a group of checks by a tag you add. An overall rate hides a failing endpoint. - A check that throws, for example
r.json()on an HTML error page, ends the iteration: later checks and requests in it never run. - Checks catch wrong answers that HTTP status codes miss, such as a maintenance page served with
200.
What is a check in k6?
A check is a call to check() from the k6 module with a value, usually a response, and an object of named conditions. Each condition is a function that returns true or false. k6 runs every condition, records each result in the checks metric, and prints the pass rate per check name at the end of the run. Checks are the k6 equivalent of assertions in other testing tools.
import http from 'k6/http';
import { check } from 'k6';
export default function () {
const res = http.get('https://test-api.example.com/users/42');
check(res, {
'status is 200': (r) => r.status === 200,
'is JSON': (r) => (r.headers['Content-Type'] || '').includes('application/json'),
'body has user': (r) => r.body.includes('"user"'),
});
}
The names are what you will read in the summary, so make them describe the expectation, not the code. In k6 v2 the end-of-test summary splits the result into checks_total, checks_succeeded and checks_failed, and counts every condition separately: 20 iterations with 3 conditions each give 60 checks.
Does a failed check fail the k6 test?
No. We ran the script above against an endpoint that answered 200 with an HTML maintenance page instead of JSON. Two of the three checks failed on every iteration, 40 of 60 in all, and k6 exited with code 0. The http_req_failed metric showed 0.00%, because every response had a successful status code. A CI job reading the exit code would have passed this build.
█ TOTAL RESULTS
checks_total.......: 60 267.587167/s
checks_succeeded...: 33.33% 20 out of 60
checks_failed......: 66.66% 40 out of 60
✓ status is 200
✗ is JSON
✗ body has user
This is intended behaviour. Grafana’s documentation says failed checks “do not cause the test to abort or finish with a failed status”, so that one bad response in a long load test does not end it. The example also shows why checks matter: the status code said everything was fine, and only the content checks noticed that users were getting a maintenance page.
How do you make checks fail a test?
Add a threshold on the checks metric. The metric is a rate, the share of conditions that passed, so checks: ['rate>0.99'] fails the run when 1% or more of the checks have failed. A failed threshold makes k6 exit with code 99, which stops a CI pipeline. The k6 documentation puts it directly: to make a check fail a test, “combine it with a Threshold”.
export const options = {
thresholds: {
checks: ['rate>0.99'],
},
};
Choose the rate from what you can tolerate, not from a round number. Under heavy load a few failures are normal, and rate==1 turns every timeout into a failed build. For a smoke test with one user, rate==1 is reasonable, since any failure there means the script or the service is broken.
How do you set a threshold for a single check?
Filter the checks metric by the check’s name or by a tag. Every check result carries a check tag with the condition’s name, so 'checks{check:status is 200}' covers only that condition. You can also pass tags as a third argument to check() and filter on those, which groups checks by endpoint or by user journey.
export const options = {
thresholds: {
checks: ['rate>0.9'], // all checks together
'checks{check:status is 200}': ['rate==1'], // one check by name
'checks{endpoint:flaky}': ['rate>0.95'], // checks carrying a custom tag
},
};
export default function () {
const api = http.get('http://127.0.0.1:8099/api');
check(api, { 'status is 200': (r) => r.status === 200 }, { endpoint: 'api' });
const flaky = http.get('http://127.0.0.1:8099/flaky'); // about 10% of responses are 500s
check(flaky, { 'flaky is 200': (r) => r.status === 200 }, { endpoint: 'flaky' });
}
In our run, the overall check rate was 97.50% and passed its threshold, the named check passed at 100%, and the tagged threshold failed at 95.00%: 38 of 40 responses from the flaky endpoint were good, which is not above 95%. k6 exited with code 99 and named the metric that crossed: thresholds on metrics 'checks{endpoint:flaky}' have been crossed. With only the overall threshold, the run would have passed, because the healthy endpoint’s checks diluted the failing one’s. Grafana’s checks page does not show the check tag filter; the run above is how we confirmed it works in v2.3.0.
What is the difference between a check and a threshold in k6?
A check tests one response and records whether it met a condition. A threshold tests a whole metric over the run, such as the 95th percentile response time or the share of checks that passed, and decides whether the test passes. Checks never change the exit code; thresholds do. Most scripts need both: checks to say what a correct response is, thresholds to say how many incorrect ones are acceptable.
| Check | Threshold | |
|---|---|---|
| Evaluates | One response (or any value) | A metric across the run |
| Written as | check(res, { name: fn }) in the test code | thresholds in options |
| Example | Status is 200, body has an id | p(95) under 500 ms, checks rate above 99% |
| Effect when it fails | Counted in checks, test continues | Test fails, exit code 99 |
| Can stop the test early | No | Yes, with abortOnFail |
A check can test timing, for example (r) => r.timings.duration < 500, but a threshold on http_req_duration usually does that job better. The check counts how many single requests were slow; the threshold on a percentile says how slow the slow ones were, which is what a service level is written in. The stress test guide shows abortOnFail thresholds stopping a run at the breaking point.
What happens when a check throws an error?
The iteration ends at that point. We wrote a check that parses the body as JSON, (r) => r.json().user.id === 42, and pointed it at the HTML maintenance page. k6 logged GoError: cannot parse json, recorded that condition as failed, and stopped the iteration. The condition after it never ran, and the next group of requests in the same iteration was never sent.
group('login', () => {
const res = http.get('http://127.0.0.1:8099/html');
check(res, {
'status is 200': (r) => r.status === 200,
'user id is 42': (r) => r.json().user.id === 42, // throws on an HTML body
'fast enough': (r) => r.timings.duration < 200, // never evaluated
});
});
group('after login', () => {
http.get('http://127.0.0.1:8099/api'); // never sent
});
Over three iterations the summary showed 6 checks instead of 9: “status is 200” passed 3 times, “user id is 42” failed 3 times, and “fast enough” did not appear at all. The exit code was 0. In a load test this is easy to miss, because the error is a log line and the iteration count looks normal, while every request after the failing check is missing from the load.
Guard the parse inside the condition so it returns false instead of throwing:
'user id is 42': (r) => {
try {
return r.json().user.id === 42;
} catch (e) {
return false;
}
},
Checking the Content-Type first, in an earlier condition, does not protect a later one: k6 runs every condition in the object even after one fails.
How do you use the result of check()?
Use its return value. check() returns true when every condition passed and false when any failed, so a script can stop a user journey that has already gone wrong. In our run, a check with one passing and one failing condition returned false, and an early return skipped the checkout request that would otherwise have run on a broken session.
const ok = check(res, {
'login 200': (r) => r.status === 200,
'got a token': (r) => (r.headers['Content-Type'] || '').includes('json') && r.body.includes('token'),
});
if (!ok) {
return; // do not continue to checkout with a failed login
}
Ending the iteration this way keeps the next requests from adding misleading errors to the results: a failed login followed by ten failed authenticated calls looks like eleven problems when there is one. The failed check still counts in the checks metric, so a threshold on it will still fail the run.
What should a k6 check verify?
Check what a real client depends on: the status code, the content type, and one or two fields the next step uses, such as an id or a token. Status alone misses wrong content served with 200, as the maintenance page showed. Avoid comparing whole bodies or large JSON documents in a load test: every condition runs on every response, and parsing large bodies costs CPU on the load generator.
Name checks so a failure explains itself without opening the script, and keep the same name across requests that test the same thing, because the summary and the check tag group results by name. In longer scripts, wrap steps in group(); the default compact summary in k6 v2.3.0 hides per-group results, and --summary-mode=full shows them, as the k6 reports guide describes. If your script is in TypeScript, checks work the same way, see the k6 TypeScript guide.
How LoadFocus uses k6 checks
LoadFocus runs k6 scripts on the stock k6 v2.3.0 binary from 25 cloud regions, with your checks and thresholds unchanged, so they run inside k6 on each load generator as they do on your machine. When you upload a script, LoadFocus warns if it contains no check() calls, since a test without checks cannot tell a correct response from a wrong one.
The verdict of a cloud run works differently. On k6 load testing with LoadFocus, a run is graded PASS or FAIL by the pass/fail thresholds set in the app (maximum p95 and p99 response time, maximum error rate, minimum throughput), not by the exit code of each generator’s k6. Use the script’s checks and thresholds to validate the script locally and in smoke tests, and the app’s thresholds to gate a release on the cloud run.
Frequently Asked Questions
Do failed checks fail a k6 test?
No. Failed checks are counted in the checks metric and shown in the summary, but the test continues and exits with code 0 if nothing else fails. In our run, 66.66% of checks failed and the exit code was still 0. Add a threshold on the checks metric, such as a rate above 0.99, to make them fail the run.
What is the difference between a check and a threshold in k6?
A check tests a single response against a condition and records pass or fail. A threshold tests a metric over the whole run, such as the 95th percentile response time or the share of checks that passed, and decides whether the test passes. Only thresholds change the exit code, to 99 when one fails.
How do you set a threshold on one specific check in k6?
Filter the checks metric by the check tag, which holds the condition’s name, for example a threshold on checks with check set to “status is 200”. You can also add your own tags as the third argument of the check call and filter on those. We confirmed both filters in k6 v2.3.0: the name filter held at 100% while the tagged threshold failed the run and the overall rate passed.
Why did my k6 check stop the iteration?
Because the check’s function threw an error. Calling json() on a response that is not JSON, or reading a property of something undefined, throws, and k6 ends the iteration there: the remaining conditions and the rest of the iteration do not run. Wrap risky code inside the condition in a try and catch block that returns false.
Can a k6 check test response time?
Yes, a condition can compare the request’s duration with a limit, and the check will count how many requests were slower. For a service level such as “95% of requests under 500 ms”, a threshold on the request duration metric is usually better, because it measures the percentile directly and fails the run when it is missed.
Sources
- Checks and thresholds: Grafana k6 documentation, grafana.com/docs/k6/latest/using-k6/checks/ and grafana.com/docs/k6/latest/using-k6/thresholds/, read 7 November 2026.
- LoadFocus k6 runs, the upload check’s warning about missing check() calls: loadfocus.com/k6-load-testing and the LoadFocus guide to running a k6 load test, read 7 November 2026.
- Test runs: k6 v2.3.0 (official macOS arm64 release, checksum-verified) against a local Node.js 23.11.1 API with a JSON endpoint, an HTML endpoint that answers 200, and an endpoint that returns 500 on about 10% of requests, on an Apple M1 Max, 7 November 2026.