Your k6 checkout checks fail every run yet CI stays green - how do you make them bind?
answer
- checks measure, thresholds judge
- one option entry does it
- target the metric, not the summary row
- checks: ['rate>0.99']
- the rate is suite-wide, not per condition
basics
~10 sAdd a threshold on k6's built-in checks metric, for example checks: ['rate>0.99']. Check results alone never change the exit status; in k6 a breached threshold is the only thing that does.
solid answer
~40 sk6 v2 keeps observation and verdict separate: `check()` records outcomes, and only a **threshold** can fail a run. Every check outcome lands on the built-in `checks` Rate metric, so the fix is one entry in `options.thresholds` — `checks: ['rate>0.99']` — after which a breach makes k6 exit non-zero and the CI step goes red. Two traps follow. You must name `checks`: `checks_failed` and `checks_succeeded` are summary display values, not registered metrics, and k6 rejects a threshold on one during configuration validation with `no metric name "checks_failed" found`, before any traffic is sent. And a rule on `checks` scores every condition in the test at once, so hundreds of passing conditions elsewhere can dilute the one failing checkout condition below the point where it breaches.
code
javascript · 17 linesimport http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 10,
duration: '30s',
// Without this block the check below can fail every single
// iteration and k6 still exits 0.
thresholds: {
checks: ['rate>0.99'],
},
};
export default function () {
const order = http.post('https://example.com/checkout', '{"cart":"c-42"}');
check(order, { 'checkout returned 201': (r) => r.status === 201 });
}go deeper
Learn the pairing: a check records the result, a threshold on the checks metric turns records into a pass or fail. Without the threshold, k6 exits zero however badly the checks went.
Be able to write the option from memory and explain why it names checks rather than checks_failed, which exists only as a summary row and is rejected during configuration validation.
Show the operational reasoning: where the threshold lives, how the pipeline consumes the exit code instead of parsing output, and how you prove the gate actually fires by breaking it once on purpose.
Argue the granularity tradeoff. One suite-wide rule on checks is cheap to maintain but dilutes a single persistent failure, while narrower rules catch it and multiply the gate surface a team has to keep honest.
## Why the pipeline stays green k6 v2 keeps two things apart on purpose. `check()` **records** whether a condition held; a **threshold** **decides** whether the run passed. A failed check writes a zero-valued sample onto the built-in `checks` Rate metric and returns `false` to your script, and that is the whole of its effect. It does not throw, it does not end the iteration, and it does not touch the process exit status. If the only thing your checkout script does is call `check()`, then `k6 run` exits `0` whether every condition passed or every one failed — and a CI step, which sees nothing but that exit code, reports success. This is the single most common surprise for people arriving from unit-testing frameworks, where an assertion failure is the verdict. In k6 the verdict has to be asked for. ## The fix, in one option Because every check outcome lands on the same metric, one threshold covers the entire suite: ```javascript export const options = { thresholds: { checks: ['rate>0.99'], }, }; ``` At the end of the run k6 evaluates that rule against the `checks` rate. If the rate has fallen below the bound, the threshold is breached, and a breached threshold is the one thing in k6 that turns a successful run into a non-zero exit (`99`). The pipeline step now goes red on exactly the condition you cared about. ## Name the metric, not the summary row The trap that costs people an afternoon is writing the rule against the wrong name: | Written as | Result | | --- | --- | | `checks: ['rate>0.99']` | works — `checks` is the registered built-in Rate metric | | `checks_failed: ['rate<0.01']` | **rejected before the test starts** | | `checks_succeeded: ['rate>0.99']` | **rejected before the test starts** | | `check: ['rate>0.99']` | **rejected** — `check` is a sample tag, not a metric | `checks_total`, `checks_succeeded` and `checks_failed` are display values the summary computes from the `checks` sink. They are not in k6's metric registry and are not emitted to output backends. k6 resolves every threshold's metric name against the registry during configuration validation, before a single request is sent, so a rule on one of them fails the run immediately with `invalid threshold defined on checks_failed; reason: no metric name "checks_failed" found` and an invalid-configuration exit. The failure mode is loud, which is a mercy — but only if you recognise the message. ## Second trap: the rule scores everything A threshold on `checks` is evaluated against **all** check samples the run produced, from every scenario, group and condition. That has a consequence people discover late: - A suite that emits 500 healthy check samples per iteration and one failing checkout sample sits at a 99.8% rate. `rate>0.99` never breaches, and the broken checkout stays green forever. - Conversely, a suite whose checks are mostly one flaky condition will breach on noise, and the team learns to ignore the gate. - The rule tells you *that* something is failing, never *which* condition. That information lives in the `check` tag on each sample and in the summary's per-check pass and fail counts. Deciding between one broad rule and a narrower one is a real design choice. What is not negotiable is that some rule must exist: without it, k6 has been told to measure and never told to judge. ## Wiring it into a pipeline 1. Add the threshold to the exported `options` object so it travels with the script rather than living in the pipeline definition. 2. Have the CI step run `k6 run script.js` and let it fail on a non-zero exit — no summary parsing, no regex over stdout. 3. Keep the `check` names stable and descriptive, since they are what the summary and the sample tags show when the gate does trip. 4. Verify the gate by breaking it once on purpose. A threshold that has never fired is indistinguishable from one that cannot. ## What a strong answer sounds like The candidate says out loud that checks are diagnostics and thresholds are verdicts, names `checks` as the metric to target, writes `checks: ['rate>0.99']` without hesitating, and volunteers that `checks_failed` cannot be used because it exists only in the summary. The weak answer reaches for parsing the printed output, or asserts that the failing checks "should have" failed the run already.
- Why does k6 reject thresholds: { checks_failed: ['rate<0.01'] } before the test even runs?`checks_failed` is a summary display value derived from the `checks` Rate sink, not a metric in k6's registry. Threshold validation resolves every rule's metric name against that registry and aborts as invalid configuration when it is missing, so the run never starts. Write the rule against `checks`.
- Does a breached checks threshold stop the k6 run at the moment it breaches?Not by default. k6 evaluates thresholds as the run proceeds but lets it finish and reports the verdict at the end, so the exit status is what changes. The threshold-object form carries an opt-in flag for aborting early; without it, a breach only decides the exit code.
- If checks are only diagnostics, why not replace them with thresholds on response metrics?Thresholds score numeric metrics; checks score arbitrary boolean conditions about a body, a JSON field or your own script state. Pairing them keeps the per-condition detail in the `checks` metric and its `check` tag while the threshold supplies the verdict the pipeline reads.
saying these in an interview costs you the question
- Puts the threshold on checks_failed instead of checks
- Believes a failing check already fails the k6 run
- Thinks CI should parse the summary text for failures
- Expects a suite-wide checks rate to name the broken condition
- Assumes k6 warns when checks fail without a threshold