skip to content

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

level: middleimportance: should knowfreq 54%

answer

  1. not JavaScript, a tiny parser
  2. aggregation, operator, number
  3. seven operators, two are equal
  4. right side is a bare float
  5. milliseconds implied, never written

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.

solid answer

~40 s

k6 parses the string itself with a tiny grammar of its own: `<aggregation> <operator> <number>`, with optional spaces around the operator. The left side is a single aggregation token such as `avg`, `rate` or `p(95)`; the right side is a plain float. The seven operators are `>`, `>=`, `<`, `<=`, `==`, `===` and `!=` - and because every sink value is a float64, `===` behaves exactly like `==`. Because the right side goes through a plain number parse, `'p(95)<500ms'` and `'rate<1%'` are both rejected before the run starts; `http_req_duration` is a time metric, so `500` already means 500 milliseconds. There are no boolean operators: to require two conditions, put two entries in the array.

code

javascript · 8 lines
javascript
export const options = {
  thresholds: {
    // AND is expressed as separate array entries, not with &&
    http_req_duration: ['p(95)<500', 'p(99) < 1500', 'avg<250'],
    // a rate is a fraction: 0.01 is one percent, not '1%'
    http_req_failed: ['rate<0.01'],
  },
};

go deeper

for a junior

Recall the three slots - aggregation, operator, number - and that the number carries no unit. Be able to write a 500 ms latency rule and a 1% error-rate rule from memory.

for a middle

Explain that k6 parses the string with its own grammar rather than running it as JavaScript, list the operators, and say why a unit suffix or a percent sign is a hard parse failure.

for a senior

Point out the mistake the parser cannot catch: a number in the wrong unit parses fine and quietly asserts the wrong budget. Know that everything else is caught before the run begins.

for a principal

Consider what the narrow grammar pushes into the script: anything compound has to become a custom metric, so decide where that computation lives and how it stays reviewable across a suite.

## The grammar, in one line k6 does not evaluate a threshold as JavaScript. The string is fed to k6's own small parser, whose whole grammar is: ``` <aggregation_method> <operator> <number> ``` Whitespace around the operator is trimmed, so `'p(95)<500'` and `'p(95) < 500'` are the same rule. Nothing else is allowed - no metric name (the map key already supplied it), no second clause, no unit suffix. ## The left-hand side The left side is a single **aggregation token**: a bare keyword such as `avg`, `min`, `max`, `med`, `count`, `rate` or `value`, or a percentile written `p(N)`. The percentile form is matched by its literal `p(` prefix and `)` suffix, which is why the space in `'p (95) < 500'` breaks it - `p (95)` is neither a keyword nor a percentile, and parsing fails. Two structural rules follow from the grammar rather than from any metric's semantics: - **The aggregation must be on the left.** Writing `'500>p(95)'` scans cleanly - k6 finds `>` and splits - but then tries to read `500` as an aggregation token and gives up. - **The percentile argument is a number between 0 and 100.** `p(99.9)` is fine; anything outside that range, or a value that is not a number, is rejected while the config is still being validated. ## The seven operators | operator | meaning | note | |---|---|---| | `>` | strictly greater | | | `>=` | greater or equal | | | `<` | strictly less | the usual choice for a latency budget | | `<=` | less or equal | | | `==` | equal | | | `===` | equal | **identical to `==` in k6** | | `!=` | not equal | | `===` is accepted because the syntax looks like JavaScript, but it is not JavaScript. Every value a k6 sink hands the comparison is a `float64`, so there is no type distinction left for a strict-equality operator to make; k6's own source says as much where the two cases share a branch. Equality operators on a percentile or an average are almost never what you want anyway - floating-point averages rarely land on a round number - so in practice threshold expressions use the four ordering operators. ## The right-hand side is a bare number The right side is parsed as a plain float and nothing else. This is where most first drafts break: 1. **`'p(95)<500ms'`** - the parse of `500ms` fails, config validation fails, the test never runs. 2. **`'p(95)<500 ms'`** - same failure; the trailing text is not stripped. 3. **`'rate<1%'`** - same again. A rate is a fraction, so one percent is written `rate<0.01`. 4. **`'http_req_duration<500'`** - the metric name belongs in the map key, not the expression, so the left side fails to parse as an aggregation. Units are implied by the metric, not written in the rule. `http_req_duration` is a **time** metric, whose samples k6 records in **milliseconds**, so `'p(95)<500'` is a 500 ms budget. `http_req_failed` is a rate whose value runs from 0 to 1, so `'rate<0.01'` is a 1% error budget. Getting this wrong is silent in a way the parser cannot catch: `'p(95)<0.5'` parses perfectly and asserts half a millisecond. ## One condition per string There are no boolean operators in the grammar. `'p(95)<500 && p(99)<1500'` does not parse, and neither does a ternary or any other JavaScript expression. Two conditions mean two entries: ```javascript export const options = { thresholds: { http_req_duration: ['p(95)<500', 'p(99)<1500'], http_req_failed: ['rate<0.01'], }, }; ``` Every entry under a metric must hold for that metric to pass, so an array of expressions is already a logical AND. There is no OR: if you need one, you have to express it as a different single comparison, or compute the value yourself into a custom metric and put the threshold on that. ## Why the strictness is a feature Because the grammar is this narrow, every threshold in a script can be parsed and checked before a single request goes out. A malformed expression, an out-of-range percentile, or a metric name that does not exist all surface as a configuration failure at startup rather than as a surprise at the end of a long run. The cost is that the syntax is unforgiving about exactly the things people write out of habit - units, percent signs, and metric names inside the expression.

  • Why does k6 accept === in a threshold when it is not evaluating JavaScript?
    The syntax was made to look familiar, and `===` is simply listed among the accepted operator tokens. Every sink value is a float64 by the time the comparison runs, so strict and loose equality cannot differ - k6 handles both in the same branch.
  • How do you express a threshold on something k6 has no aggregation for, such as a ratio you compute yourself?
    Record it into a custom metric from the script, then put an ordinary expression on that metric's name. The grammar never grows; you move the arithmetic into the script and leave the rule as one aggregation, one operator, one number.
  • What does 'p(95)<0.5' assert on http_req_duration in k6?
    A budget of half a millisecond. `http_req_duration` is a time metric recorded in milliseconds, so the bare number is always milliseconds. The expression parses perfectly, which is why a unit mistake here fails the run rather than the config.

It reads like a JavaScript comparison but it is closer to a cron field: a fixed slot for each part, no expressions, and anything you add out of habit is a syntax error rather than a nuance.

saying these in an interview costs you the question

  • writes a duration suffix such as 500ms on the right side
  • expresses an error budget as 1% instead of 0.01
  • joins two conditions with && inside one string
  • repeats the metric name inside the expression
  • assumes === differs from == in a k6 threshold