skip to content

Verdict Gating

How a k6 run decides pass or fail: soft checks that only record what happened, and declared thresholds that fail the process. Interviewers open here because that split is k6's differentiator.

on this pageshow

explore

questions

27

In k6, what happens to the running iteration when a check() condition evaluates to false?

level: juniorimportance: must knowfreq 74%

answer

  1. observation, not control flow
  2. the iteration keeps going
  3. one sample per check key
  4. built-in checks rate metric
  5. only a threshold changes the verdict

basics

~10 s

Nothing halts. k6 records the failure on its built-in checks metric, check() returns false, and the iteration carries straight on to the next statement. The run's exit status is unaffected.

solid answer

~40 s

In k6 v2 a failed `check()` is a recorded observation, not a control-flow event. Each key in the object you pass emits one sample on the built-in `checks` Rate metric — `1` for a pass, `0` for a fail — tagged `check: <name>`, and the call returns `false` if any entry failed. It does not throw, does not skip the rest of the iteration, and does not on its own make k6 exit non-zero. So in a checkout script the condition on the order-confirmation status can fail while the same iteration goes on to request the receipt and finish normally. To turn that recorded failure into a verdict you add a threshold on the `checks` metric, for example `checks: ['rate>0.99']`.

code

javascript · 14 lines
javascript
import http from 'k6/http';
import { check } from 'k6';

export default function () {
  const order = http.post('https://example.com/checkout', '{"cart":"c-42"}');

  // Fails on a 500 - and changes nothing about the flow.
  check(order, {
    'checkout returned 201': (r) => r.status === 201,
  });

  // Still runs, in the same iteration, right after the failed check.
  http.get('https://example.com/receipt');
}

go deeper

for a junior

Remember the headline: in k6 a failed check records a result and lets the iteration run on. It is not an assert, and on its own it never turns a green run red.

for a middle

Be able to say what the failure actually produces - one zero-valued sample on the built-in checks Rate metric, carrying a check tag with that condition's name - and that the return value is a boolean you may branch on yourself.

for a senior

Show that you treat checks as diagnostics until something binds them. Explain how you would attach a threshold to the checks metric so a failing checkout condition stops a deploy, and what a run reports when you do not.

for a principal

Weigh how much of a suite's check surface should be bound at all: one global rule on the checks metric is cheap but blurs which condition broke, while per-condition rules are precise and multiply what the team has to maintain.

## Checks are observations, not assertions Every testing tool has a way of saying "this should be true". Most implement it as an **assertion**: the condition is evaluated, and if it comes out false the framework throws, the current test stops, and the run is marked failed. k6 v2 deliberately does not work that way. `check()` evaluates the condition, writes the result down, and hands control straight back to your script. Nothing is thrown, nothing is skipped, and the run's verdict is untouched. The reason is the workload k6 is built for. A load test is not a handful of cases that pass or fail; it is thousands of iterations, and one bad response in ten thousand is usually information rather than a failure. So k6 splits the two jobs apart: `check()` **measures** correctness, and a **threshold** **judges** it. ## What k6 does for each condition For every key in the object you pass as the second argument, k6 performs the same sequence, whether the condition is true or false: 1. Evaluate the condition, calling it with the first argument when it is a function. 2. Coerce whatever came back to a boolean. 3. Push exactly one sample onto the built-in **`checks`** metric — a **Rate** metric — valued `1` for a pass and `0` for a fail. 4. Tag that sample `check: <the object key>`, because `check` is one of k6's default system tags. 5. Continue to the next key. A false result sets the call's return value to `false` but does not end the loop, so every condition in the object still gets evaluated and recorded. When the last key has been processed, `check()` returns a boolean and your script resumes at the very next statement — the same statement it would have reached had every condition passed. ## What explicitly does not happen - **No exception.** A false condition is a value, not an error. There is nothing for a `try`/`catch` around the call to catch. - **No early exit from the iteration.** The lines after the `check()` call run normally. - **No change to the exit status.** k6's exit code is decided by thresholds; check results feed a metric, and a metric on its own decides nothing. - **No effect on iteration accounting.** The iteration is still counted as a full iteration and still emits `iteration_duration`. Nothing in k6's execution statistics marks it as tainted. - **No suppression of later checks.** A second `check()` call later in the same iteration behaves exactly as if the first had passed. | Behaviour on failure | Classic assertion | k6 `check()` | | --- | --- | --- | | Raises an exception | yes | no | | Stops the current unit of work | yes | no | | Marks the run failed | yes | no | | Records a measurement | usually not | yes, one sample on `checks` | | Names the failing condition | in the stack trace | in the `check` sample tag | ## The checkout iteration, concretely Take an iteration that posts a cart to a checkout endpoint, checks that the response status is `201`, and then fetches the receipt. If the checkout endpoint answers `500`, the condition is false. k6 emits a zero-valued sample tagged `check: checkout returned 201`, `check()` evaluates to `false`, and the very next line — the receipt request — is issued anyway, against an order that was never created. The iteration finishes, `iteration_duration` is recorded, the VU starts its next iteration, and `k6 run` eventually exits `0`. The failure is fully visible in the summary and completely inert as a verdict. That is not a bug; it is the contract. If you want the receipt request skipped, you write the branch yourself: `check()` gives you a boolean precisely so you can. Testing it and returning early, or calling `fail()` from the `k6` module, ends that one iteration — but that is your control flow, not the check's, and neither of them changes the run's exit status either. ## Making a failed check bind Because every check outcome lands on one metric, one threshold covers the lot: ```javascript export const options = { thresholds: { checks: ['rate>0.99'] }, }; ``` Now the failures are still recorded exactly as before, but at the end of the run k6 evaluates the rule against the `checks` rate and, if it is breached, exits non-zero. A CI step reading that exit code finally goes red. Without such a rule, no number of failed checks changes anything a pipeline can see. ## Where candidates go wrong - Describing `check()` as an assert and expecting the iteration to unwind. - Assuming k6 stops evaluating the object's remaining keys after the first false one. - Believing that a high failure percentage in the summary is itself a failing run. - Wiring CI to grep the summary text instead of adding a threshold and reading the exit code. - Forgetting that the receipt-style follow-up request in the same iteration still executes against a system state the failed check just told you is wrong.

  • Does a failed k6 check() reduce the iterations count or mark the iteration incomplete?
    No. k6 counts it as a full iteration and emits `iteration_duration` as usual. The only trace of the failure is a zero-valued sample on the `checks` metric, its `check` tag, and the `checks_failed` row in the end-of-test summary. Nothing in k6's execution accounting distinguishes an iteration that contained a failed check.
  • If check() never aborts, how do you stop a k6 iteration as soon as a check fails?
    Branch on the boolean the call gives back — `if (!check(res, {...})) { return; }` — or call `fail()` from the `k6` module, which throws and ends that one iteration. Both are your control flow rather than the check's, and neither changes the run's exit status.
  • Do the remaining entries in a k6 check() object still run after one of them returns false?
    Yes. k6 walks every key in the object and emits a sample for each, aggregating only the return value at the end, so one `false` does not short-circuit the rest. A two-entry call therefore reports one pass and one fail in the summary.

A failed check is the inspector's tick box on a factory line, not the emergency stop: the item gets marked and the line keeps moving. Someone still has to write the rule that says how many bad marks end the shift.

saying these in an interview costs you the question

  • Says a failed check aborts the iteration like an assert
  • Believes a failed check makes k6 exit non-zero
  • Thinks one false entry skips the remaining checks in the object
  • Assumes checks alone are enough to gate a CI pipeline
  • Expects the iteration to be dropped from the iterations count
open as a page

In k6, how do you call check() from the k6 module, and what does each argument do?

level: juniorimportance: must knowfreq 78%

basics

~20 s

k6's check(), from the k6 module, has the signature check(val, sets, [tags]): a value, an object whose keys are check names and whose values are conditions called with that value, and optional extra tags. It returns a boolean.

open as a page

In k6, what exit code does a run return when one of its thresholds is breached?

level: juniorimportance: must knowfreq 72%

basics

~20 s

k6 exits with code 99 when at least one threshold is breached, and 0 when they all hold. That single integer is the whole machine-readable verdict a CI job reads; the printed summary is for humans.

open as a page

In k6, why can a threshold of `checks: ['rate>90']` never pass, however many checks succeed?

level: juniorimportance: must knowfreq 72%

basics

~20 s

k6's built-in checks metric is a Rate, and the only aggregation a Rate threshold accepts is rate, which is a fraction from 0.00 to 1.00 rather than a percentage. So rate>90 is unreachable; write rate>0.9.

open as a page

In k6, what does a threshold key like http_req_duration{status:200} do that a plain metric name does not?

level: juniorimportance: must knowfreq 62%

basics

~10 s

The braces create a sub-metric: k6 registers a filtered copy of http_req_duration that receives only the samples whose tags contain status:200, and evaluates the threshold against that copy instead of the whole metric.

open as a page

How do you declare a threshold in k6's exported options object, and what entry forms exist?

level: juniorimportance: must knowfreq 72%

basics

~10 s

k6's options.thresholds maps a metric name to an array of entries. Each entry is either an expression string like 'p(95)<500' or an object carrying threshold, abortOnFail and delayAbortEval. One array may mix both forms.

open as a page

In k6, exactly what makes a check() call return false rather than true?

level: middleimportance: must knowfreq 62%

basics

~10 s

A k6 check() call returns false when any entry in its set evaluates to a JavaScript falsy value. Every entry is still evaluated and recorded, and an empty set returns true because nothing failed.

open as a page

In k6, which threshold aggregations does each metric type allow, and what happens on a mismatch?

level: middleimportance: must knowfreq 66%

basics

~20 s

In k6 v2 a Counter takes count and rate, a Gauge takes value, a Rate takes rate, and a Trend takes avg, min, max, med and p(N). Any other pairing is rejected as invalid configuration before the test starts.

open as a page

Your k6 checkout checks fail every run yet CI stays green - how do you make them bind?

level: seniorimportance: must knowfreq 68%

basics

~10 s

Add 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.

open as a page

How does a failed k6 check() surface in the checks metric and the end-of-test summary?

level: middleimportance: should knowfreq 52%

basics

~20 s

Each check key emits one sample on k6's built-in checks Rate metric, 1 for a pass and 0 for a fail, tagged with the condition's name. The summary derives checks_total, checks_succeeded and checks_failed from that single metric.

open as a page

In k6, does calling fail() from the k6 module fail the whole test run?

level: middleimportance: should knowfreq 41%

basics

~10 s

No. k6's fail() throws an error that ends only the current iteration. k6 logs it, still counts the iteration as finished, starts the next one, and leaves the run's exit status untouched.

open as a page

How do k6's exec.test.abort() and exec.test.fail() differ in effect and in exit code?

level: middleimportance: should knowfreq 44%

basics

~10 s

exec.test.abort() stops a k6 run immediately and exits 108; exec.test.fail() marks the run failed, lets every iteration finish, and exits 110. Both take an optional message and both still run teardown().

open as a page

What does k6's --no-thresholds flag change about a run's exit code and its validation?

level: middleimportance: should knowfreq 46%

basics

~20 s

k6's --no-thresholds flag switches off threshold evaluation and threshold validation for the whole run, so a breach can no longer produce exit 99 and a malformed rule no longer produces exit 104. The run exits 0.

open as a page

Why can a k6 threshold on http_req_duration{vu:3} never match a sample, and what does k6 report?

level: middleimportance: should knowfreq 34%

basics

~20 s

k6 stores vu and iter as non-indexed sample metadata rather than as indexed tags, and a selector can only test indexed tags. k6 logs a startup warning naming that threshold, then runs with a permanently empty sub-metric.

open as a page

In a k6 threshold expression such as p(95)<500, what may legally appear on each side?

level: middleimportance: should knowfreq 54%

basics

~20 s

A k6 threshold expression is exactly one aggregation token, one comparison operator from > >= < <= == === and !=, and a bare number. No units, no second condition, no JavaScript. Time metrics take their number in milliseconds.

open as a page

In k6, why can the built-in check() not take an async predicate, and what do you use instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

k6's built-in check() evaluates predicates synchronously and rejects any async function with an error pointing at the k6-utils jslib check. expect() is not in the k6 binary at all; it comes from the remotely imported k6-testing jslib.

open as a page

In k6, what happens when a predicate inside a check() set throws an exception?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A throwing predicate in a k6 check() set is recorded as a failed check under its own name, then the exception escapes check(): the remaining entries in that set never run, and the iteration ends there.

open as a page

In k6, which exit code wins when a run both breaches a threshold and marks itself failed?

level: seniorimportance: should knowfreq 34%

basics

~10 s

k6 exits 110, not 99. The threshold code is attached only if the run does not already carry an error, so exec.test.fail(), exec.test.abort() and script exceptions all take precedence over a breach.

open as a page

In k6, `rate` is legal on both a Counter and a Rate metric, so what does each one compute?

level: seniorimportance: should knowfreq 44%

basics

~20 s

On a k6 Counter, rate is the running total divided by the elapsed run time, so it is events per second and unbounded. On a Rate metric, rate is non-zero samples over total samples, a proportion between 0 and 1. Same token, different units.

open as a page

In k6, what happens to a threshold on http_req_duration{endpoint:checkout} if no sample ever carries that tag?

level: seniorimportance: should knowfreq 50%

basics

~20 s

The threshold passes. k6 creates the sub-metric at startup, no sample ever matches it, and on an empty Trend sink every aggregation reads 0 - so p(95)<500 is satisfied and the run ends green with the endpoint unmeasured.

open as a page

What do abortOnFail and delayAbortEval do to a k6 threshold, and when exactly does the abort fire?

level: seniorimportance: should knowfreq 41%

basics

~20 s

abortOnFail makes k6 stop a running test once that threshold fails; delayAbortEval holds the abort back until the run has lasted longer than the given duration. k6 re-evaluates thresholds every two seconds, so the abort lands on the next tick.

open as a page

What does the optional third argument to k6's check() do, and what may it contain?

level: middleimportance: nice to knowfreq 46%

basics

~20 s

check()'s optional third argument in k6 is a flat object of extra tags applied only to the results that one call emits. Values must be strings, booleans or numbers, and every value is stored as a string.

open as a page

In k6, which aggregations may a Trend threshold use, and why is `count<100` rejected on one?

level: middleimportance: nice to knowfreq 37%

basics

~20 s

A k6 Trend threshold accepts exactly avg, min, max, med and p(N) with N from 0 to 100. count is not among them, because counting samples belongs to the Counter type, so count<100 on a Trend is rejected as invalid configuration.

open as a page

Which k6 threshold keys are rejected before the run starts, and which malformed-looking ones are accepted?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

k6 rejects a threshold key with unmatched braces, a brace that is not final, a pair lacking a value after its colon, or an unknown metric name. It accepts whitespace, quotes, colons inside values, and tag keys nothing emits.

open as a page

In k6, what happens if a condition inside check() throws instead of returning false?

level: seniorimportance: nice to knowfreq 26%

basics

~10 s

k6 records that entry as a failure - a zero-valued sample on the checks metric - and then rethrows. The remaining keys in the object never run, and the exception ends the iteration.

open as a page

How often does k6 re-evaluate thresholds during a run, and what does the final pass judge differently?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

k6 re-evaluates thresholds every two seconds while a run is live, skipping metrics with no samples yet. The final pass after the run skips nothing, so a rule on a never-exercised metric breaches there and exits 99.

open as a page

Do k6's p(95) latency and http_req_failed rate thresholds pass on a run that made zero requests?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Both pass. k6's final evaluation judges every declared threshold, and a metric with no samples offers the comparison either nothing at all, which counts as a pass, or a zero that beats any upper bound.

open as a page