What does the optional third argument to k6's check() do, and what may it contain?
answer
- third argument, and it is optional
- a flat object of extra tags
- strings, booleans and numbers only
- scoped to this call's check results
basics
~20 scheck()'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 sIn 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 linesimport 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
Just know it exists and is optional: check(value, conditions, extraTags). You will write hundreds of two-argument calls before you need the third.
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.
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.
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