In k6, how does exec.vu.tags set a metric tag mid-iteration, and which values does it accept?
answer
- a live view, not a copy
- set it, then delete it
- strings, booleans and numbers only
- reset on activation, not per iteration
basics
~20 sexec.vu.tags, from k6/execution, is a live view of the running VU's tag set: assigning to a key tags every sample the VU emits from then on, until you delete it. Only string, boolean and number values are accepted.
solid answer
~40 s`exec.vu.tags`, from the `k6/execution` module, is a live view of the running VU's tag set rather than a copy. Assigning `exec.vu.tags.flow = 'checkout'` adds that tag to every sample the VU emits from that point on, and `delete exec.vu.tags.flow` removes it; reading works too, so `exec.vu.tags['scenario']` returns the current scenario name. Only string, boolean and number values are accepted — anything else throws a `TypeError` saying only String, Boolean and Number types are accepted as metric tag values. The trap is lifetime: k6 rebuilds a VU's tag set when the VU is activated for a scenario, **not** at the start of each iteration, so a tag set in one iteration is still set in the next unless you delete it. That is why the documented example ends with an explicit `delete`.
code
javascript · 15 linesimport exec from 'k6/execution';
import http from 'k6/http';
export const options = { vus: 1, iterations: 2 };
export default function () {
exec.vu.tags.flow = 'checkout';
http.get('https://quickpizza.grafana.com/');
http.get('https://quickpizza.grafana.com/api/headers');
// Without this, flow=checkout survives into the next iteration
delete exec.vu.tags.flow;
http.get('https://quickpizza.grafana.com/api/status/200');
}go deeper
Know it exists: import exec from k6/execution and assign to exec.vu.tags to add a tag from code rather than at a call site. Delete the key when you are done with it.
Explain the value rule - string, boolean or number, everything else throws - and that the object is a live view of the VU's tag set, so the change takes effect immediately.
Lead with the lifetime trap: the tag set is rebuilt on VU activation, not per iteration, so a conditional assignment leaks into later iterations of that same VU.
Judge when a spanning dimension is worth it at all, and keep its value set small - a tag assigned per iteration from unbounded data is the quickest route to k6's time-series warning.
## The object In k6 v2, `k6/execution` exposes four namespaces — `instance`, `scenario`, `test` and `vu` — and `exec.vu.tags` is a live view of the tag set the currently running VU stamps onto everything it emits. It is not a snapshot you pass somewhere; assigning to a property of it changes the VU's tag set immediately. ```javascript import exec from 'k6/execution'; import http from 'k6/http'; export default function () { exec.vu.tags.flow = 'checkout'; http.get('https://shop.test/cart'); // tagged flow=checkout http.get('https://shop.test/pay'); // tagged flow=checkout delete exec.vu.tags.flow; http.get('https://shop.test/health'); // no flow tag } ``` The same object is also reachable as `exec.vu.metrics.tags`, with a sibling `exec.vu.metrics.metadata` for non-indexed values. Reading works too: `exec.vu.tags['scenario']` returns the current scenario name, because system tags live in the same set. ## Why it exists The per-call tag arguments — a `tags` object in an HTTP params argument, the third argument to `check()`, the second argument to a custom metric's `.add()` — each tag exactly one thing. When a dimension has to span a stretch of an iteration, repeating it at every call site is both noisy and easy to get wrong. `exec.vu.tags` is the mechanism for a value that: - is only known at runtime, such as which of several data-driven paths this iteration took; - has to cover several calls, including calls made inside helper functions you did not want to thread a tags argument through; - has to cover nested `group()` calls, so that a container dimension survives the group path changing underneath it. ## The value rule k6 accepts **string, boolean and number** values and stores their string form. Anything else throws immediately: ```javascript exec.vu.tags.team = { id: 7 }; // TypeError: invalid value for metric tag 'team': only String, Boolean and // Number types are accepted as a metric tag values ``` This is the same rule that governs `tags` in an HTTP params object and the tag argument to `check()` and `.add()` — one shared conversion path, one shared error. There is no JSON serialisation fallback, so a nested object has to be flattened into separate tags yourself. ## The lifetime trap This is the part that separates people who have used it from people who have read about it. **k6 rebuilds a VU's tag set when the VU is activated for a scenario, not at the start of every iteration.** On activation, k6 seeds the tag set from the test-wide run tags plus any scenario-level tags and clears the metadata. Between iterations of that same activation it only refreshes the `iter` value. The consequence: a tag you assign in one iteration is **still set in the next iteration** of the same VU. A conditional assignment like `if (isNewUser) exec.vu.tags.cohort = 'new';` leaks `cohort=new` into every subsequent iteration that VU runs, including the ones for returning users. The fix is to delete it, or to assign on both branches: | Pattern | Result | | --- | --- | | Assign, never delete | tag persists for every later iteration of that VU | | Assign on one branch only | earlier value leaks into later iterations | | Assign, then `delete` before the iteration ends | scoped to the stretch you intended | | Assign unconditionally on every branch | overwritten each iteration, no leak | ## Interaction with groups and system tags - Setting a key that is also a system tag overwrites it for that VU. `exec.vu.tags.name = 'X'` behaves like a manual `name` tag; it is legal and occasionally useful, but surprising to a reader. - Do not assign to `group` by hand. `group()` reads the current `group` tag, appends `::name`, and restores the old value when the callback returns; a hand-set value gets folded into the path. - A tag set from code is an ordinary indexed tag, so a sub-metric selector of the form `metric{key:value}` matches it exactly as it would a system tag. That is usually the whole reason for setting one. ## Practical rules 1. Assign and `delete` in the same function, so the scope is visible in one screen. 2. Prefer a per-call `tags` argument when the dimension covers one call — it cannot leak. 3. Keep the value set small; a tag set per iteration from unbounded data is the fastest way to push a run past k6's 100,000-time-series warning. 4. Remember it is per VU: two VUs running the same code hold independent tag sets.
- Why does k6's documented exec.vu.tags example end with a delete?Because k6 rebuilds a VU's tag set on activation for a scenario, not per iteration. Without the delete the tag stays set for every later iteration that VU runs, silently tagging traffic it has nothing to do with.
- Can you read an existing tag through exec.vu.tags in k6?Yes — it is a live view of the whole tag set, so `exec.vu.tags['scenario']` returns the scenario name and any run tag is readable by key. Non-indexed values sit under `exec.vu.metrics.metadata` instead.
- What is the difference between exec.vu.metrics.tags and exec.vu.metrics.metadata?`tags` are indexed and form part of a sample's series identity, so a `metric{key:value}` selector can match them. `metadata` travels with the sample but is not indexed, which is where k6 puts the non-indexable `vu` and `iter` system tags.
saying these in an interview costs you the question
- Assumes the tag resets at the start of every iteration
- Passes an object as a tag value and expects JSON
- Thinks exec.vu.tags is shared between VUs
- Sets the group tag by hand alongside group()
- Believes it only affects HTTP metrics