skip to content

Aggregation Method Limits

Which aggregation a rule may use depends on the metric's type, and the wrong pairing is rejected as bad configuration. Interviewers use it because a percentile rule on the wrong metric never runs.

on this pageshow

explore

questions

4

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

level: juniorimportance: must knowfreq 72%

answer

  1. the metric type decides the vocabulary
  2. checks is a Rate, not a Counter
  3. rate is a proportion, not a percent
  4. non-zero samples over total samples
  5. the summary prints the percent form

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.

solid answer

~40 s

In k6 v2, `checks` is a built-in metric of type **Rate**, and a Rate metric admits exactly one threshold aggregation: `rate`. That `rate` value is computed as non-zero samples divided by total samples, so it lives strictly between `0.00` and `1.00`. An expression such as `rate>90` therefore parses fine, passes k6's type validation, and can never be satisfied, because the left-hand side tops out at `1.0`. The `90` comes from the end-of-test summary, which humanises a Rate-typed metric by multiplying it by 100 and appending a percent sign, so a run that shows `checks 95.00%` is really holding `0.95`. The correct expression for "at least 90% of checks passed" is `checks: ['rate>0.9']`.

code

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

export const options = {
  thresholds: {
    // WRONG: a Rate's `rate` never exceeds 1.00, so this can never pass.
    // checks: ['rate>90'],

    // RIGHT: 90% of checks must pass.
    checks: ['rate>0.9'],

    // http_req_failed is a Rate too: under 1% failed requests.
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  const res = http.get('https://quickpizza.grafana.com');
  check(res, { 'status is 200': (r) => r.status === 200 });
}

go deeper

for a junior

Remember that k6's checks metric is a Rate and that a Rate threshold compares a fraction between 0 and 1. Ninety percent is 0.9, not 90.

for a middle

Be able to explain that the rate value is non-zero samples divided by total samples, and that the summary multiplies it by 100 purely for display.

for a senior

Point out that k6 validates only the type-to-aggregation pairing, never the right-hand value, so an unreachable rule ships green through review and only bites at the end of a real run.

for a principal

The tradeoff to raise is that a threshold vocabulary tied to metric type catches whole classes of nonsense for free but leaves unit errors entirely to the author, so unit conventions have to be enforced elsewhere.

## Why `checks` only speaks one aggregation k6 registers a fixed set of built-in metrics before your script's `default` function ever runs, and each one is created with a **metric type**. `checks` is registered as a **Rate**. That type is not cosmetic: in k6 the legal aggregation methods inside a threshold expression are a pure function of the metric's type, and the Rate row of that table has exactly one entry. | Metric type | Aggregations a threshold may use | |---|---| | Counter | `count`, `rate` | | Gauge | `value` | | **Rate** | **`rate`** | | Trend | `avg`, `min`, `max`, `med`, `p(N)` | So `checks: ['avg<0.1']`, `checks: ['count>500']` and `checks: ['value>0.9']` are all rejected outright — k6 reports an *invalid threshold* and refuses to start the test. Only `rate` gets through. In k6 v2 this is checked while the configuration is being assembled, before a single virtual user is spawned. ## What the `rate` number actually is A Rate sink keeps two integers: how many samples it has seen in total, and how many of those were non-zero. When k6 evaluates a threshold it divides the second by the first: - a passing check contributes a `1`; - a failing check contributes a `0`; - the aggregation `rate` is `non-zero / total`. That quotient is a **proportion**. Its floor is `0.0` (nothing passed) and its ceiling is `1.0` (everything passed). There is no arithmetic path by which it reaches `90`. `rate>90` is not rejected — it is perfectly well-formed, and `checks` genuinely supports `rate` — it is simply an assertion that a number no larger than one exceeds ninety. ## Where the `90` comes from The confusion is manufactured by k6's own end-of-test summary. When the summary renders a metric whose type is `rate`, it does not print the stored value. It multiplies by 100, truncates to two decimals and appends `%`. A run in which 95% of checks passed therefore prints something like `checks.......: 95.00%`, and the threshold line above it prints `✓ 'rate>0.9'`. The displayed figure and the compared figure are the same fact in two different units, and only the compared figure is the one your expression sees. ## Writing it correctly 1. Decide the share you want as a **fraction**: 90% is `0.9`, 99% is `0.99`, 99.9% is `0.999`. 2. Put it on the right-hand side of a `rate` comparison: `checks: ['rate>0.9']`. 3. Read the summary's percentage as a rendering of that same fraction, not as the value to type. The same rule governs every Rate metric, not just `checks`. `http_req_failed` is also a built-in Rate, which is why the canonical error-budget threshold is written `http_req_failed: ['rate<0.01']` — under one percent — and never `rate<1`, which would be a tautology that always holds. ## The edge case worth knowing If the run emits **no** samples at all on a Rate metric — no `check()` ever executed, say — the sink's total is zero. Rather than divide by zero, k6 simply does not publish a `rate` value for that evaluation, and a threshold with nothing to compare against is treated as **passing**. A `checks` threshold on a run that never reached its checks therefore reports green, which is a very different outcome from the same rule on a Counter, where an unsampled counter still resolves `count` to `0` and a rule like `count>100` genuinely breaches. ## The same rule across every metric type The constraint is not special to `checks`. k6 derives the legal left-hand side of any threshold from the type of the metric named on the key, and the four types have four disjoint vocabularies. A **Counter** such as `http_reqs` answers `count` and `rate`. A **Gauge** such as `vus` answers only `value`. A **Rate** such as `checks` answers only `rate`. A **Trend** such as `http_req_duration` answers `avg`, `min`, `max`, `med` and `p(N)`. Nothing about the metric's *name* participates in that decision, which is why a rule copied from one metric to another so often stops being valid. Two practical habits follow from this. First, look up the type before writing the expression rather than reasoning from what the metric appears to measure. Second, treat the right-hand number as part of the same decision: a Rate's operand is always a fraction, a Gauge's is in whatever unit you fed the gauge, and a Trend's is in the unit of the samples you added — milliseconds for the built-in timing metrics, but whatever you chose for a custom one. ## Summary - `checks` is a built-in **Rate** metric in k6 v2, so its threshold vocabulary is the single token `rate`. - `rate` means *non-zero samples over total samples*: a fraction in `[0.00, 1.00]`. - `rate>90` type-checks, starts the run, and can never be true. - The summary's `95.00%` is `0.95` rendered for humans. - Write the fraction: `checks: ['rate>0.9']`.

  • Why does k6 accept `checks: ['rate>90']` at startup instead of rejecting it?
    k6's pre-run validation only asks whether the metric's *type* supports the aggregation token. `checks` is a Rate and Rate supports `rate`, so the pairing is legal. k6 never inspects the right-hand number for plausibility, so an unreachable comparison is a runtime-only defect that surfaces as a breached threshold at the end of the run.
  • What is reported if a k6 run finishes without executing a single `check()`?
    The `checks` Rate sink has a total of zero, so k6 publishes no `rate` value for it. A threshold whose aggregation has no value to compare against is treated as passing, so `checks: ['rate>0.9']` reports green on a run where no check ever ran. Guard against that with a separate rule on a metric that is always sampled, such as `iterations`.
  • Does `http_req_failed` take the same aggregation as `checks`?
    Yes. `http_req_failed` is also a built-in Rate metric in k6 v2, so `rate` is its only legal threshold aggregation and the right-hand side is again a fraction. `http_req_failed: ['rate<0.01']` means fewer than one percent of requests were marked as failures.

A shop's system stores a discount as 0.2 while the receipt prints 20% off. Typing 20 into the field that expects 0.2 is not rejected, it is just a discount nobody will ever earn.

saying these in an interview costs you the question

  • Writing rate>90 because the summary shows a percentage
  • Believing checks is a Counter, so count>100 is a valid rule
  • Assuming k6 rejects a right-hand value that is out of range
  • Thinking avg or med can be used on a Rate metric
  • Reading a green checks threshold as proof checks actually ran
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

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, 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