skip to content

Per-User State

A module-scope variable looks global but is private to one virtual user, and k6 hands each user numbers saying which user and which iteration this is, in the test and in the instance.

on this pageshow

explore

questions

5

In a k6 script, what is the scope and lifetime of a variable declared at module level?

level: middleimportance: must knowfreq 72%

answer

  1. per user, not per process
  2. init code runs once per VU
  3. each VU gets its own runtime
  4. iterations never re-run init
  5. one copy per VU, in memory

basics

~20 s

Module-scope variables in k6 are private to one virtual user, not global. Every VU runs the script's init code in its own JavaScript runtime, so each VU gets its own copy, which then survives every iteration that VU runs.

solid answer

~40 s

Code outside k6's lifecycle functions is *init code*, and k6 executes it once per VU inside a JavaScript runtime belonging to that VU alone. A `let` or `const` declared there is therefore **per-VU state**: created when the VU is initialised, private to it, and untouched by iteration boundaries, so a value written on iteration 0 is still there on iteration 500. No other VU can read or write it, and k6 never collects or reports it. That makes module scope the right home for something one simulated user should build once and reuse, such as a session token. It is the wrong home for anything you want shared: 500 VUs means 500 independent copies held in memory at the same time.

code

javascript · 20 lines
javascript
import http from 'k6/http';
import exec from 'k6/execution';

// Init code: runs once per VU, in that VU's own runtime.
let sessionToken = null;
let loginsPerformed = 0;

export const options = { vus: 5, iterations: 20 };

export default function () {
  if (sessionToken === null) {
    const res = http.post('https://quickpizza.grafana.com/api/users/token/login', '{}');
    sessionToken = res.json('token');
    loginsPerformed++; // still 1 on this VU's twentieth iteration
  }
  http.get('https://quickpizza.grafana.com/api/headers', {
    headers: { Authorization: `Bearer ${sessionToken}` },
  });
  console.log(`vu ${exec.vu.idInTest} logged in ${loginsPerformed} time(s)`);
}

go deeper

for a junior

Remember the shape: code outside the exported functions runs once per virtual user, and what it creates belongs to that user only. Nothing at the top of a k6 file is shared across VUs.

for a middle

Be able to explain why: k6 builds a separate JavaScript runtime per VU and evaluates the whole script inside it, and it never re-runs that code between iterations. Both properties follow from that one fact.

for a senior

Show the operational consequence. Memory multiplies by VU count, so a large parsed dataset at module level is a load-generator problem before it is a script problem, and no VU can rely on work another VU did.

for a principal

The tradeoff is per-VU realism against generator cost. Per-VU state models independent users faithfully but scales linearly with concurrency, so it is worth deciding as a team which data belongs per VU and which must be a single shared read-only copy.

## What counts as module scope in a k6 test file A k6 test file holds exactly two kinds of code. Anything written **inside** an exported lifecycle function -- `default`, `setup`, `teardown`, `handleSummary` -- runs when k6 calls that function. Everything else at the top level of the file is **init code**: the `import` statements, the `export const options = {...}` block, and any `const`, `let` or helper function you declare between them. A variable declared out there is what this question calls a **module-scope variable**. The word "module" invites the wrong mental model. It sounds like one shared box that the whole run reads from, the way a static field is shared inside a single operating-system process. In k6 it is nothing of the sort. k6 builds each virtual user by creating a **fresh JavaScript runtime for that VU** -- k6 v2 embeds the Sobek engine -- and then evaluating the whole script inside it. Fifty VUs means the file is evaluated fifty times and there are fifty independent copies of every top-level binding, none of which can see the others. ## The two halves of the answer - **Scope is one VU.** No other VU can read or write your copy. There is no message passing, no shared mutable object, and no lock, because there is nothing to contend over. - **Lifetime is the VU's whole life.** The value is created when k6 initialises that VU and lasts until the run ends or the VU is discarded. - **An iteration boundary does not reset it.** k6 does not re-run the init code before each iteration; it only calls the exported function again. A value written on iteration 0 is still there on iteration 500. - **A scenario change does not reset it either.** When k6 reuses an existing VU for a second scenario it re-activates the same runtime rather than rebuilding it, so the state carries over. `exec.vu.iterationInInstance` behaves the same way -- it keeps counting rather than restarting. - **Nothing is aggregated or written back.** k6 never collects module-scope values, never sums them, and never puts them in the end-of-test summary. - **Everything is discarded at the end of the run.** Per-VU state is memory, not storage. ## Where a value can live in a k6 script | Where you put it | Who can see it | When it goes away | | --- | --- | --- | | a `let` at module level | only the VU whose init code created it | end of the run | | a `let` inside `default` | only the current iteration | end of that iteration | | the VU's HTTP cookie jar | only that VU | start of every iteration, unless `noCookiesReset: true` | | `new SharedArray()` from `k6/data` | every VU, read-only | end of the run | The last row is the only sharing k6 offers inside a run, and it is deliberately read-only. If you want VUs to influence one another, k6 has no mechanism for it -- that is a property of the execution model, not an oversight. ## The worked case: a session the VU builds once The most common legitimate use is a credential a single simulated user should obtain once and then reuse. Declare a `let token = null` at module level, and on each iteration log in only when it is still `null`. Every VU performs exactly one login no matter how many iterations it runs, and every VU carries its own distinct session, which is usually what you want when the point is to model many concurrent users rather than one user hammering an endpoint. The mirror-image trap is worth naming, because it looks identical in the source. k6 gives each VU an HTTP cookie jar, but by default it replaces that jar with an empty one at the **start of every iteration**. So a session established through a `Set-Cookie` header vanishes at the iteration boundary while a token you stashed in a module-scope variable does not. Two pieces of "per-VU session state" in the same script, two different lifetimes. ## Consequences worth checking before you ship a script 1. **Do not size your data by the file.** A 40 MB JSON file parsed into a module-scope constant is 40 MB *per VU*. At 300 VUs that is 12 GB on the load generator. This is precisely the case `SharedArray` exists for. 2. **Do not build cross-VU counters.** A module-scope `let requests = 0` reports only what one VU did; to get a run-wide number you need a k6 metric, not a variable. 3. **Do not assume ordering.** Because each VU is independent, "VU 1 warmed the cache so VU 7 can skip it" is never true. Every VU has to be able to run the script from a cold start. ## What to say in an interview State the two properties in one breath -- **private to a VU, and it survives iterations** -- and explain *why* rather than just asserting it: init code runs once per VU inside that VU's own JavaScript runtime, so there is one copy per VU and nothing re-runs it between iterations. Then name the consequence that actually bites in production: memory multiplies by VU count, and no VU can observe another. Everything else about k6 per-user state follows from those two sentences.

  • Can two k6 VUs communicate through a module-scope variable?
    No. Each VU evaluates the script in its own JavaScript runtime, so there is one independent copy per VU and no path between them. The only sharing k6 offers inside a run is a single read-only copy created with `new SharedArray()` from `k6/data`; there is no writable channel between VUs at all.
  • Does a module-scope variable survive when a k6 VU is reused by a second scenario?
    Yes. k6 re-activates the existing VU rather than rebuilding its runtime, so every module-scope value carries over. `exec.vu.iterationInInstance` behaves the same way and keeps counting, while `exec.vu.iterationInScenario` restarts at 0 for the new scenario.
  • What is the memory cost of parsing a large file into a module-scope constant in k6?
    It multiplies by the VU count, because every VU parses and stores the file separately. A 40 MB JSON document becomes 12 GB at 300 VUs. `SharedArray` from `k6/data` exists precisely to keep one copy in the k6 process instead.

Every VU is handed an identical toolbox at the start of the shift. Scratching a mark on your own toolbox does not change anyone else's, and the mark is still there when you pick it up for the next job.

saying these in an interview costs you the question

  • Says module-scope variables are global and shared by all k6 VUs
  • Thinks k6 re-runs the init code before every iteration
  • Expects one VU's cached login to be reused by another VU
  • Believes k6 reports module-scope values in the end-of-test summary
  • Assumes 500 VUs share one parsed copy of a file loaded at module level
open as a page

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

level: juniorimportance: should knowfreq 64%

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.

open as a page

In k6, how do exec.scenario.iterationInTest and exec.vu.iterationInInstance differ?

level: middleimportance: should knowfreq 51%

basics

~20 s

exec.scenario.iterationInTest numbers the iterations of one scenario across every VU and every k6 instance, so no two iterations share a value. exec.vu.iterationInInstance counts only what a single VU has run, so many VUs report 0 at the same moment.

open as a page

A k6 VU logs in on its first iteration but is unauthenticated on the next. Why?

level: seniorimportance: should knowfreq 44%

basics

~20 s

k6 replaces each VU's cookie jar with an empty one at the start of every iteration, so a session cookie set on iteration 0 is gone on iteration 1. Set noCookiesReset to true, or keep the credential in a module-scope variable.

open as a page

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

level: middleimportance: nice to knowfreq 33%

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.

open as a page