skip to content

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

level: middleimportance: nice to knowfreq 46%

answer

  1. third argument, and it is optional
  2. a flat object of extra tags
  3. strings, booleans and numbers only
  4. scoped to this call's check results

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.

solid answer

~40 s

In k6 v2 the signature is `check(val, sets, [tags])`, and the third argument is a flat `{key: value}` object whose pairs are attached as extra tags to the results **this call** produces — every entry in the set gets the same extras. It is applied to a copy of the VU's current tags, so it does not modify the VU's tags for later code and it does not retag the HTTP request that produced the value; those samples were emitted before `check()` ran. Only `String`, `Boolean` and `Number` values are accepted: anything else — a nested object, an array, a function — fails with *"only String, Boolean and Number types are accepted as a metric tag values"*. Accepted values are stored as strings, so `{ attempt: 1 }` becomes `attempt="1"`.

code

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

export default function () {
  const res = http.post('https://example.com/login', JSON.stringify({ user: 'alice' }));

  check(
    res,
    {
      'login status is 200': (r) => r.status === 200,
      'login body has a token': (r) => r.json('token') !== null,
    },
    { flow: 'login', attempt: 1 } // both results get flow="login" attempt="1"
  );
}

go deeper

for a junior

Just know it exists and is optional: check(value, conditions, extraTags). You will write hundreds of two-argument calls before you need the third.

for a middle

Explain the type rule — strings, booleans and numbers only, all stored as strings — and that the extras land on the check results of that one call, not on the request.

for a senior

Reach for it when one shared helper emits the same check names from several flows, and keep the values bounded so you are not creating a new result series per iteration.

for a principal

The tradeoff is cardinality against attribution: every extra tag value multiplies the series a run carries, so agree as a team which dimensions are worth that cost.

## The argument nobody passes until they need it `check()`'s full signature in k6 v2 is `check(val, sets, [tags])`. The third argument is optional, and most scripts never supply it. When you do, it is a **flat object of extra tags** attached to the check results that this one call emits. ```javascript check(res, { 'login status is 200': (r) => r.status === 200 }, { flow: 'login' }); ``` Two properties of that sentence carry all the weight: *extra*, and *this one call*. ## What may be a tag value k6 validates each value in the object and accepts a deliberately narrow set of types. | value in the tags object | accepted | stored as | |---|---|---| | `'login'` (string) | yes | `"login"` | | `1` (number) | yes | `"1"` | | `false` (boolean) | yes | `"false"` | | `{ n: 1 }` (object) | **no** | rejected | | `[1, 2]` (array) | **no** | rejected | | `() => 1` (function) | **no** | rejected | Anything outside the accepted three fails the call with *"invalid value for metric tag '<key>': only String, Boolean and Number types are accepted as a metric tag values"*. Note the second column: **every accepted value is stored as a string**. `{ attempt: 1 }` produces `attempt="1"`, not the integer `1`. Tags are strings all the way down, so any numeric comparison you were hoping to do later is a string comparison instead. Passing `null` or `undefined` as the whole third argument is a harmless no-op rather than an error, which is convenient when the extras are computed. ## Scope: this call's results, and nothing else The extras are merged into a **copy** of the VU's current tags, taken at the moment `check()` runs. Three consequences follow, and each is a misconception worth naming: - **It does not tag the request.** The HTTP call that produced `res` emitted its own measurements before `check()` was reached. Adding `{ flow: 'login' }` here cannot reach back and label them. - **It does not tag the VU.** The copy is discarded when the call returns, so nothing you pass here is visible to the next request, the next check, or the next iteration. - **It applies to every entry in the set uniformly.** There is no way to give one key in the set different extras from another; if you need that, make two calls. One detail of ordering follows from that: the tag object is read **once, before any entry in the set is evaluated**. A tag whose value depends on how the checks turned out therefore cannot exist — by the time an entry has passed or failed, the extras for that call are already fixed and applied. If what you actually want is a tag on the request or on everything the VU does from here on, this argument is the wrong tool — it is scoped to the check results of a single call by construction. ## The worked case ```javascript import http from 'k6/http'; import { check } from 'k6'; export default function () { const res = http.post('https://example.com/login', JSON.stringify({ user: 'alice' })); check( res, { 'login status is 200': (r) => r.status === 200, 'login body has a token': (r) => r.json('token') !== null, }, { flow: 'login', attempt: 1 } // both entries get flow="login" attempt="1" ); } ``` Both named results carry `flow="login"` and `attempt="1"`. The `http.post` above them carries neither. ## When it earns its place The honest answer is: rarely, and always for the same reason — you have the *same* check names appearing from more than one context and you need to tell those contexts apart afterwards. Typical cases: 1. **One helper, many callers.** A shared `validateLoginResponse(res)` function used by several flows emits identical check names; a `{ flow: … }` extra recovers which flow produced which result. 2. **Retry rounds.** Distinguishing the first attempt from a retry when both run the same conditions. 3. **Parameterised data sets.** The same conditions run against several accounts or regions, where the account is not part of the check's name. What it is *not* for is putting high-cardinality values — a user id, a request id, a timestamp — into the tag object. Every distinct value multiplies the number of distinct result series the run has to carry, and a check tagged with a unique id per iteration produces one series per iteration. Keep the extras to a small, bounded set of values you would actually group by.

  • Does k6's check() third argument tag the HTTP request that produced the value?
    No. The request emitted its own measurements before `check()` was called, so nothing passed here can reach them. The extras are merged into a copy of the VU's tags and apply only to the check results that this one call produces.
  • What happens if a value in k6's check() tag object is a nested object?
    The call fails with *"only String, Boolean and Number types are accepted as a metric tag values"*, naming the offending key. Flatten it yourself — `{ 'region': r.region }` rather than `{ 'meta': { region: r.region } }` — or serialise it to a string first.

saying these in an interview costs you the question

  • Thinks the third argument tags the request as well
  • Passes a nested object or array as a tag value
  • Expects a numeric tag to stay a number
  • Believes the extras persist for the rest of the iteration
  • Puts a unique id per iteration into the tag object