Why does k6 reject open(), require() and new Trend() anywhere except a script's init context?
answer
- mirror image of the http rule
- init environment, not VU state
- filesystem, module resolver, metric registry
- declare in init, use per iteration
basics
~20 sk6's open(), require() and metric constructors need the init environment - the filesystem, module resolver and metric registry - which k6 discards the moment init ends. Outside init they throw: init-stage only, or metrics must be declared in the init context.
solid answer
~40 sk6 keeps an **init environment** alive only while it evaluates a script's module scope; it holds the filesystem, the module resolver and the metric registry. `open()`, `require()` and the `Counter`, `Gauge`, `Rate` and `Trend` constructors all reach into it, so k6 refuses them once it is gone — with *"the \"open\" function is only available in the init stage"* and *"metrics must be declared in the init context"*. This is the exact mirror of the VU-state rule: `http.get()` and `.add()` are refused *inside* init for the opposite reason. The design keeps a run reproducible and lets k6 record the complete file and module set up front.
code
javascript · 11 linesimport { Trend } from 'k6/metrics';
// init-only: needs the init environment
const config = JSON.parse(open('./config.json'));
const loginTime = new Trend('login_time', true);
export default function () {
// open('./config.json'); // throws: only available in the init stage
// new Trend('late_metric'); // throws: metrics must be declared in the init context
loginTime.add(config.budgetMs); // legal here, and only here
}go deeper
Learn the placement: read files and declare custom metrics at the top of the script, never inside the default function.
Explain that these calls need the init environment holding the filesystem, module resolver and metric registry, which k6 discards once init ends.
Connect it to reproducibility and archiving: k6 records the complete file and module closure during the load-time pass, so nothing may be added later.
Weigh what the constraint forbids: no per-VU data sourcing from disk, which pushes large or per-user datasets toward setup() or an external service.
## The mirror image of the forbidden list k6 splits its API in two along the same seam. Most calls need **VU state** and are refused while it is absent — that is the familiar "no `http.get()` in init" rule. A smaller set needs the **init environment** instead, and is refused once that environment is gone. `open()`, `require()` and the four custom metric constructors are in the second group. The init environment exists only while k6 is evaluating the module scope. It carries the filesystem handles, the module resolver, and the metric registry. k6 clears it the moment init finishes, and only then attaches the VU state. So the two conditions are exactly complementary: a call is either init-only or init-forbidden, never both and never neither. Learning one half of the split gives you the other half for free, which is why it is worth stating as a rule rather than memorising two lists. ## What each one says when you get it wrong | Call | Placed outside init, k6 says | |---|---| | `open('./users.json')` | `the "open" function is only available in the init stage (i.e. the global scope)` | | `require('./helpers.js')` | `the "require" function is only available in the init stage (i.e. the global scope)` | | `new Trend('t')`, `new Counter('c')`, `new Gauge('g')`, `new Rate('r')` | `metrics must be declared in the init context` | The `open()` and `require()` messages share one template because both are globals k6 installs on the runtime with the same guard. ## Why the filesystem is init-only Two reasons, and the second is the one candidates miss. 1. **Reproducibility.** If a VU could read a file mid-run, two VUs could observe different bytes for the same path — one before an editor saved over it, one after — and the run would stop being repeatable. 2. **Bundling.** After the load-time init pass, k6 locks the filesystem to exactly the set of paths that pass touched, and locks the module resolver the same way. That recorded set is what makes a script self-contained: k6 knows the complete file and module closure of the test because it was all declared up front, with no branch able to add to it later. The practical consequence is that `open()` must be called **unconditionally**. A call hidden behind a runtime condition may not run during the load-time pass, and then fails when a later VU does take that branch, because the path was never recorded. ## Why metrics must be declared in init A custom metric is not a local variable — it is an entry in the test's metric registry, and the registry is reached through the init environment. Declaring it in init guarantees that every VU registers the same metric under the same name and type before any sample is produced, so the aggregation at the end is over one coherent series rather than a set of series that appear at different moments in the run. The counterpart rule follows from the same design: - **construct in init** — `const loginTime = new Trend('login_time', true);` - **add outside init** — `loginTime.add(res.timings.duration);` inside the `default` function. Trying it the other way round produces the mirror-image errors: `metrics must be declared in the init context` for a constructor in VU code, and `Adding to metrics in the init context is not supported` for an `.add()` at module scope. The pair of messages is the fastest way to confirm which half of the split you have violated. ## The shape this pushes scripts into The rule quietly enforces a good structure. Everything a test needs to know — its fixtures, its module graph, its metric names, its `options` — is declared in one place, once, before the first request. The `default` function is left with the work that actually varies per iteration. - Fixture parsing happens once per VU rather than once per iteration. - The module graph and the file set are fixed and inspectable before the run begins. - Metric names cannot appear and disappear part-way through a run. - A reviewer can read the top of the file and know every external input the test has. When you hit one of these errors, the fix is never to find a way around the guard — there is no flag that relaxes it, and no ordering trick that smuggles a late `require()` through. It is to hoist the declaration to module scope and leave only the per-iteration use behind. If the thing you wanted really cannot be known until the test is running, it belongs in `setup()` and its result travels to the VUs as data.
- Is open() legal inside the callback passed to a k6 SharedArray constructor?Yes. That callback is invoked while the constructor runs, and the constructor itself is init-only, so the `open()` call is still executing inside the init context. `new SharedArray('users', () => JSON.parse(open('./users.json')).users)` is the standard form and gets checked by exactly the same guard.
- What error does k6 give for a require() call inside the default function?`the "require" function is only available in the init stage (i.e. the global scope)`. k6 installs `require` as a global whose guard checks whether VU state exists; once it does, the call is refused. Beyond that, the module resolver is locked after the load-time init pass, so no new module could be resolved anyway.
saying these in an interview costs you the question
- Thinks open() is banned in VU code purely for performance
- Tries to lazily require a module inside the default function
- Constructs a new Trend per iteration inside default
- Believes the same guard governs open() and http.get()
- Assumes open() can read a different file for each VU