skip to content

In k6, how do you tag one HTTP request versus tagging every sample the run emits?

level: middleimportance: should knowfreq 62%

answer

  1. call site versus whole run
  2. three call sites take a tag object
  3. options.tags reaches every metric
  4. the flag and the variable disagree

basics

~10 s

Per-entity tags go at the call site: a tags object in an HTTP params argument, check()'s optional third argument, or a custom metric's add(). Test-wide tags go in options.tags, --tag NAME=VALUE, or K6_TAGS=name:value.

solid answer

~40 s

Two scopes, two mechanisms. For one entity you pass a tag object at the call site: a `tags` key inside an HTTP request's params object, the optional third argument to `check()`, or the second argument to a custom metric's `.add()`. That tag lands only on the samples that call emits, which is why `check()` needs its own argument even when the request already had one. For the whole run you use the `tags` option, which seeds every VU's tag set so that even `iterations` and `vus` carry it. You can set it as `options.tags` in the script, as repeatable `--tag NAME=VALUE` flags, in `K6_TAGS` as a comma-separated list of `NAME:VALUE` pairs, or under `tags` in a JSON config file. Mind the separator: `--tag` wants `=`, `K6_TAGS` wants `:`.

code

javascript · 18 lines
javascript
import http from 'k6/http';
import { check } from 'k6';
import { Trend } from 'k6/metrics';

const waiting = new Trend('waiting_time', true);

export const options = {
  tags: { build: __ENV.BUILD_ID || 'local' },
  vus: 1,
  iterations: 1,
};

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

go deeper

for a junior

Know the two everyday moves: a tags object inside an http.get params argument, and a tags key in the exported options object for the whole run.

for a middle

Enumerate all three per-entity call sites and all four run-wide sources, and get the separators right - equals for the flag, colon for the environment variable.

for a senior

Explain that run tags seed the VU's tag set at activation rather than being stitched on per sample, which is why they reach iterations and vus as well as HTTP metrics.

for a principal

Talk about key discipline across a suite: one spelling per dimension, values from small fixed sets, and never a per-request identifier as a tag value.

## Two scopes, two mechanisms k6 v2 lets you attach your own key/value tags at two very different scopes, and they are configured in completely different places. - **Per entity.** One request, one `check()` call, or one custom-metric sample gets a tag that nothing else in the run carries. This is an argument you pass in the script, at the call site. - **Test-wide.** Every sample the run emits — built-in and custom, from every VU and every scenario — carries the same value. This is the `tags` option, set outside the call site. Both end up in the same place: the sample's tag set, indistinguishable from a system tag once emitted. The difference is purely where you declare them and how much they cover. ## Tagging one entity Three call sites accept a user tag object, one per taggable thing: | Entity | Where the tags go | Example | | --- | --- | --- | | HTTP request | a `tags` key in the params argument | `http.get(url, { tags: { endpoint: 'products' } })` | | Check | the optional third argument to `check()` | `check(res, { 'is 200': r => r.status === 200 }, { endpoint: 'products' })` | | Custom metric | the second argument to `.add()` | `myTrend.add(res.timings.waiting, { endpoint: 'products' })` | The tag lands only on the samples that call produces. A `tags` object on an `http.get` tags that request's `http_req_duration`, `http_req_waiting`, `http_reqs` and the rest of its family; it does not tag the `check()` on the same response, which is why the third argument to `check()` exists at all. Tag values are restricted: k6 accepts **string, boolean and number** values and converts them to their string form. Anything else — an object, an array, `null` — raises a `TypeError` saying only string, boolean and number types are accepted as metric tag values. ## Tagging the whole run Test-wide tags are the `tags` option, and k6 gives you four ways to supply it: 1. **In the script**, as the `tags` key of the exported `options` object: `export const options = { tags: { build: '1421' } };` 2. **On the command line**, with one or more `--tag NAME=VALUE` flags. The flag is repeatable, so `--tag build=1421 --tag env=staging` sets two. 3. **In the environment**, with `K6_TAGS` holding a comma-separated list of pairs: `K6_TAGS=build:1421,env:staging`. 4. **In a JSON config file**, under the `tags` key, with the same shape as the in-script option. Note the separator changes between the two command-line-shaped sources: **`--tag` uses `=` and `K6_TAGS` uses `:`**. `--tag build=1421` is right and `--tag build:1421` is wrong — k6 rejects a `--tag` value with no `=`, an empty name or an empty value with a parse error. Conversely `K6_TAGS=build=1421` is not parsed as a pair — the whole entry is rejected as an invalid map item. This asymmetry between the flag and the environment variable is the most common mistake with k6 run tags, and it only bites once you start driving k6 from a pipeline instead of from the script. ## Where the tag actually attaches Test-wide tags are not stitched onto samples one by one at emit time. When k6 activates a VU for a scenario it builds that VU's **starting tag set** from the run tags, and everything that VU emits inherits it — including `iterations` and `iteration_duration`, which you never explicitly touch. The `vus` and `vus_max` gauges are not emitted by a VU at all, so the scheduler stamps the run tags onto them directly. Either way the result is the same: a run tag is on every sample in the run, not just on the HTTP ones. Scenarios can also carry their own `tags` in the `scenarios` map, layered over the run tags for the VUs running that scenario. That belongs to the scenario configuration rather than to tagging as such, but it is worth knowing the layer exists when a tag value you did not expect turns up. ## Choosing a key - Keep keys stable across scripts. A sub-metric selector such as `http_req_duration{endpoint:products}` only matches samples whose key is spelled `endpoint`; k6 never warns that a sibling script spelled the same dimension `api`. - Prefer a value drawn from a small fixed set — an endpoint name, a scenario label, a build id. Every distinct combination of tag values on a metric is a separate time series k6 holds in memory, and k6 logs a warning once a run passes 100,000 of them. - Do not reuse a system tag's key for your own dimension. Writing `tags: { name: … }` on a request is meaningful and documented, but writing `tags: { group: … }` fights with `group()`. - A tag whose value is unique per request — an order id, a session token — is the classic way to make a k6 run consume far more memory than it should.

  • Which value types does k6 accept for a user-defined tag?
    String, boolean and number, stored as their string form. Anything else — an object, an array — raises a `TypeError` saying only String, Boolean and Number types are accepted as metric tag values. There is no JSON fallback.
  • Does a tags object on an http.get in k6 also tag the check() on that response?
    No. A per-entity tag covers only the samples that call emits, so the `checks` sample is untagged. Pass the same object as `check()`'s third argument, or set the dimension once on `exec.vu.tags` so it spans both.
  • Which metrics does a test-wide tag in k6 reach?
    All of them. k6 seeds each VU's tag set from the run tags at activation, so `iterations`, `group_duration` and every custom metric carry it; the scheduler stamps the same tags onto `vus` and `vus_max`, which no VU emits. Nothing in the run escapes it.

saying these in an interview costs you the question

  • Thinks options.tags only applies to HTTP metrics
  • Writes K6_TAGS=name=value instead of name:value
  • Passes --tag name:value and expects it to parse
  • Believes a request's tags object also tags its check
  • Tags a request with an order id or session token