skip to content

What value does k6's global __VU hold while a script's init code is running?

level: middleimportance: nice to knowfreq 33%

answer

  1. set before the script is evaluated
  2. not always zero in init
  3. zero for service passes only
  4. the module API throws, the global does not
  5. the iteration global is absent entirely

basics

~20 s

__VU holds that VU's instance-local id: k6 sets it on the runtime before evaluating the script, so VU 3's init code sees 3. It is 0 only for the load-time pass that reads options, and for setup, teardown and handleSummary.

solid answer

~40 s

k6 creates the VU's JavaScript runtime, writes `__VU` onto it, and only then evaluates the script, so during **VU 3's own init pass `__VU === 3`**. The widely repeated claim that `__VU` is always `0` in init code is a half-truth: it is `0` for the one-off load-time pass k6 makes to read the exported `options`, and for the service VUs that run `setup()`, `teardown()` and `handleSummary()`. Every load-generating VU sees its real id, starting at 1, from the first line of init. That is what makes per-VU init work at all, such as `const shard = __VU % 4`. `exec.vu` is the opposite: reading it in init throws, because the VU's state object does not exist yet.

code

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

// Init code: runs once per VU, in that VU's runtime.
console.log(`init sees __VU = ${__VU}`); // 0 once, then 1, 2, 3 ...
const myShard = __VU % 4;

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

export default function () {
  // exec.vu is readable here, but throws if read above.
  console.log(`shard ${myShard}, idInInstance ${exec.vu.idInInstance}`);
}

go deeper

for a junior

You are unlikely to be asked this. If it comes up, the useful fact is that __VU already carries the virtual user's number while the top of the file runs, which is why per-user setup can happen there.

for a middle

Distinguish the passes. k6 evaluates the script once at load time to read options, once per service VU for setup and teardown, and once per load-generating VU; only the last group sees a non-zero __VU.

for a senior

Point out the practical trap: any init-time branch on __VU also executes with __VU === 0 during the load-time pass, so an expression like users[__VU - 1] indexes -1 there and needs a guard.

for a principal

Treat this as a reason to keep identity-dependent logic out of init entirely. Init-time branching on a VU number couples the script to k6's construction order, which is not a contract worth building a shared test library on.

## The short answer, and why it is not the popular one While a VU's init code runs, `__VU` already holds **that VU's own instance-local id**. k6 creates the VU's JavaScript runtime, sets `__VU` on it, and only then evaluates the script. So VU 3's init pass sees `__VU === 3`, VU 4's sees `4`, and so on. The widely repeated claim that "`__VU` is 0 in the init context" is a half-truth that has been copied into a great deal of public k6 material. `__VU` really is `0` during init -- but only for specific init passes, not for all of them. ## When `__VU` is 0 - **The load-time pass.** Before any load starts, k6 evaluates the script once in a throwaway runtime to read the exported `options` object and build the bundle. That pass runs with id 0. It is also the pass that decides which files `open()` is allowed to touch later, which is why the error message for a late `open()` mentions `__VU==0`. - **`setup()` and `teardown()`.** k6 runs both in a service VU created with id 0, so `__VU` is `0` inside those functions and inside the init pass that builds that VU. - **`handleSummary()`.** Same mechanism, same `0`. Every VU that actually generates load gets a real id, starting at 1, and sees it from the very first line of its init code. ## Why this matters in practice Init code is the one place where you can do per-VU setup work cheaply -- pick a data shard, derive a username, choose a base URL -- because it runs once per VU rather than once per iteration. That is only possible because the VU's identity is already known there. If `__VU` really were always 0, a line such as `const shard = __VU % 4` at module level would put every VU on shard 0, and a great many k6 scripts in the wild would be quietly broken. The consequence to keep in mind is the mirror image: because that same init code also runs during the load-time pass and for the service VUs, any init-time branch on `__VU` executes at least once with `__VU === 0`. Code such as `users[__VU - 1]` therefore indexes `-1` on that pass. Guard it, or put the lookup inside the exported function. ## `__ITER` and `exec.vu` in init Neither of the other two identity routes is available: | Expression | In init code | | --- | --- | | `__VU` | works; holds this VU's instance-local id (`0` for the load-time and service passes) | | `__ITER` | not set at all -- k6 assigns it just before each iteration begins | | `exec.vu.idInInstance` / `idInTest` | throws *getting VU information in the init context is not supported* | | `exec.scenario.*`, `exec.instance.*` | throw for the same reason | `exec.vu` throws because those 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. `__VU` avoids the problem because it is a plain value written onto the runtime much earlier, at construction time. `exec.test.abort()` is the one part of `k6/execution` that k6 documents as usable during initialisation. ## How to check it yourself Put `console.log('init', __VU)` at the top level of a script and run it with a handful of VUs. You get one line with `0` from the load-time pass, one `0` per service VU if the script exports `setup()` or `teardown()`, and then one line per load-generating VU with `1`, `2`, `3` and so on. That output is the whole answer, and it takes about ten seconds to produce. ## Interview framing This is a differentiator question rather than a screening one. Nobody's k6 competence should be judged on it. But an answer that distinguishes "the init pass k6 makes to read `options`" from "each VU's own init pass" shows that the candidate has a real model of how k6 constructs virtual users, and the follow-up -- why `exec.vu` throws where `__VU` works -- separates people who have read the behaviour from people who have reasoned about it. ## Version note All of this describes k6 v2.x. `__VU` and `__ITER` remain available but are documented as the discouraged form; `k6/execution` is the current API and its `vu` and `scenario` objects are deliberately unavailable until a VU is running a scenario.

  • Why does reading `exec.vu.idInTest` in a k6 script's init code throw?
    Because `exec.vu` is an accessor backed by the VU's runtime state object, and k6 does not attach that until the VU is activated for a scenario. Reading it during init raises *getting VU information in the init context is not supported*. `__VU` works there because it is a plain value written onto the runtime much earlier.
  • Is `__ITER` available in a k6 script's init code?
    No. k6 assigns `__ITER` onto the VU's runtime immediately before each iteration begins, so during init it has never been set and does not exist. The first iteration sees `0`, because the iteration counter is zero-based.

saying these in an interview costs you the question

  • Says __VU is always 0 in the k6 init context
  • Expects exec.vu.idInTest to be readable in k6 init code
  • Thinks __VU is assigned only once the default function starts
  • Assumes __ITER is 0 during init rather than absent
  • Believes the load-time options pass runs as VU 1