skip to content

In k6, how do exec.vu.idInInstance and exec.vu.idInTest differ, and what does __VU hold?

level: juniorimportance: should knowfreq 64%

answer

  1. two scopes, one number each
  2. counting starts at one
  3. instance-local versus run-wide
  4. the legacy global tracks the local id
  5. setup and teardown report zero

basics

~10 s

k6 gives every virtual user two one-based ids: exec.vu.idInInstance is unique inside one k6 process, and exec.vu.idInTest is unique across the whole run. The legacy global __VU holds the instance-local number.

solid answer

~40 s

k6 assigns each VU two identifiers, both starting at **1**. `exec.vu.idInInstance` is unique among the VUs of a single k6 process; `exec.vu.idInTest` is unique across the entire run, so it stays distinct even when the run is spread over several k6 instances. On an ordinary local `k6 run` there is only one instance, so the two numbers are identical, which is why the difference is easy to miss. The older global `__VU` still exists in k6 v2 and holds the **instance-local** value, the same number as `idInInstance`, not `idInTest`. It is `0` inside `setup()` and `teardown()`, because k6 runs those in a throwaway VU. Since ids start at 1 and arrays at 0, indexing a per-VU dataset needs `- 1`.

code

javascript · 17 lines
javascript
import exec from 'k6/execution';

export const options = { vus: 3, iterations: 6 };

export function setup() {
  console.log(`setup runs with __VU = ${__VU}`); // 0
}

export default function () {
  console.log(
    `__VU=${__VU} idInInstance=${exec.vu.idInInstance} idInTest=${exec.vu.idInTest}`
  );
}

export function teardown() {
  console.log(`teardown runs with __VU = ${__VU}`); // 0
}

go deeper

for a junior

Know that k6 numbers virtual users from 1, that exec.vu.idInTest is the run-wide id, and that __VU is the older global holding the per-process one. The off-by-one against array indexes is the practical part.

for a middle

Explain why two ids exist at all: a single k6 process numbers its own VUs from 1, so those numbers repeat once a run is spread across processes, and only idInTest stays unique.

for a senior

Bring up the failure mode. A dataset keyed on __VU is correct on a laptop and silently duplicates rows the moment the run is split, and the symptom is data collisions rather than a script error.

for a principal

Worth standardising: pick exec.vu.idInTest as the team's default identity for anything that has to be distinct, so scripts stay correct if the run is ever executed by more than one instance.

## Two identifiers, and why there are two k6 gives every virtual user two identity numbers, both exposed on the `vu` object of the `k6/execution` module: - **`exec.vu.idInInstance`** -- unique among the VUs handled by a single k6 process. - **`exec.vu.idInTest`** -- unique across the whole test run, however many k6 processes take part in it. Both are allocated **from 1**, not from 0. That is a deliberate historical choice in k6 and it is the single most common source of off-by-one bugs in k6 scripts, because JavaScript arrays start at 0. On an ordinary `k6 run script.js` there is exactly one k6 process, so the two numbers are always equal and the distinction is invisible. It becomes visible only when one run is executed by more than one k6 instance: each instance numbers its own VUs from 1, so instance-local ids repeat across instances, while `idInTest` is allocated so that no two VUs anywhere in the run share a value. That is why k6's own data-parameterisation examples reach for `idInTest` when the point is "give this simulated user a row nobody else gets". ## Where `__VU` fits `__VU` is the older global k6 has exposed since long before the `k6/execution` module existed. It is still present in k6 v2, still works, and is documented as the discouraged option. The part people get wrong is *which* of the two ids it holds: > `__VU` is the **instance-local** value -- the same number as `exec.vu.idInInstance`, not > `exec.vu.idInTest`. k6 sets `__VU` directly on the VU's JavaScript runtime when it builds the VU, which is why it is readable in init code, where `exec.vu` is not yet available. There is one more value `__VU` takes that surprises people. k6 runs `setup()` and `teardown()` in a throwaway VU that is not part of the load, and that VU is created with id 0. So inside `setup()` and `teardown()`, `__VU` is `0` -- it is not the first load-generating VU, and it is not `undefined`. ## The three values side by side | Expression | Range | Unique across | Available in init code | | --- | --- | --- | --- | | `__VU` | 1..N per instance; `0` in `setup`/`teardown` | one k6 instance | yes | | `exec.vu.idInInstance` | 1..N per instance | one k6 instance | no, it throws | | `exec.vu.idInTest` | 1..N across the run | the whole test run | no, it throws | ## Why init code can read `__VU` but not `exec.vu` There is one more asymmetry worth knowing, because it decides where you can use each form. `__VU` is a plain value that k6 writes onto the VU's JavaScript runtime at the moment it builds that runtime, before a single line of the script has been evaluated. The `exec.vu` properties are accessors backed by the VU's runtime **state** object, and k6 does not attach that object until the VU is activated for a scenario. The practical consequence: - In init code -- everything outside the exported functions -- `__VU` works and already holds the VU's real id, while `exec.vu.idInInstance` throws *getting VU information in the init context is not supported*. - Inside `default`, `setup` or `teardown`, both work. So a script that wants to derive something per VU at init time, such as a data shard or a base URL, has `__VU` as its only option, and that is the one place where reaching for the discouraged global is still the correct choice. ## The off-by-one, spelled out Because VU ids start at 1 and arrays are indexed from 0, the correct expression for "this VU's row" is `users[exec.vu.idInTest - 1]`. Omit the `- 1` and two things go wrong at once: the first row of the dataset is never used by anybody, and the highest-numbered VU indexes one element past the end and gets `undefined`. That failure is quiet -- the request still goes out, just with `undefined` where the username should be -- so it usually shows up as a wall of 401s rather than as a script error. ## Choosing between them 1. **Reach for `exec.vu.idInTest`** whenever the number has to be unique for the run: assigning a distinct user account, a distinct tenant, or a distinct shard of a dataset. It is the only one of the three that keeps that promise if the run is ever split across instances. 2. **Reach for `exec.vu.idInInstance` or `__VU`** when you only need to tell this process's VUs apart -- log correlation, a local round-robin, picking one VU to do something once on this box. 3. **Do not reach for either as a stable identity across runs.** Ids are allocated per run; VU 7 in today's run has nothing to do with VU 7 in yesterday's. ## What an interviewer is listening for The give-away answer is "`__VU` is the VU number" and nothing else. The answer that lands names the two scopes, says that both start at 1, and knows that `__VU` tracks the instance-local one -- then adds the `setup()`/`teardown()` zero as the detail that shows you have actually watched the logs of a k6 run rather than read a blog post about one.

  • When can `exec.vu.idInInstance` and `exec.vu.idInTest` return different numbers in k6?
    Only when one test run is executed by more than one k6 instance. Each instance numbers its own VUs from 1, so instance-local ids repeat across instances, while `idInTest` is allocated so that every VU in the run gets a distinct value. A plain local `k6 run` has a single instance, so the two always match.
  • Why do k6 examples write `users[exec.vu.idInTest - 1]` rather than `users[exec.vu.idInTest]`?
    Because k6 numbers VUs from 1 while JavaScript arrays are indexed from 0. Without the `- 1`, VU 1 reads the second row and the first row is never used, and the highest-numbered VU indexes one past the end and gets `undefined` instead of a record.

saying these in an interview costs you the question

  • Says k6 VU ids start at 0 like array indexes
  • Treats __VU as globally unique in a run split across instances
  • Thinks __VU holds the same value as exec.vu.idInTest
  • Expects __VU to be a real VU number inside setup() or teardown()
  • Uses __VU to number iterations rather than virtual users