skip to content

Runtime and Extension

Everything between a .js file and a request on the wire: the JavaScript VM k6 embeds, what it will and will not import, and how that surface gets widened. The limits bite early.

on this pageshow

explore

questions

19

Why does a k6 script's import of './helpers' fail when './helpers.js' works?

level: juniorimportance: must knowfreq 72%

answer

  1. browser-like, not Node-like
  2. the loader guesses nothing
  3. no index.js, no node_modules walk
  4. write the suffix yourself

basics

~20 s

k6 resolves import specifiers the way a browser does, not the way Node does: it never appends .js, never falls back to index.js and never searches node_modules. The extension-less path names a file that does not exist, so the load fails.

solid answer

~40 s

k6 has its own loader and does not implement the Node.js module resolution algorithm. A specifier starting with `.` or `/` is joined onto the importing file's URL and read from disk **exactly as written** — no extension probing, no `index.js` fallback for a directory, no walk up through `node_modules`. So `./helpers` resolves to a path with no extension, nothing is there, and k6 aborts before the test starts with `The moduleSpecifier "./helpers" couldn't be found on local disk.` The k6 docs call this browser-like module resolution and state that file names for imports must be fully specified. The same applies to TypeScript: k6 transpiles with esbuild purely on the `.ts` filename suffix, so that extension has to be written out too.

code

javascript · 6 lines
javascript
// tests/smoke.js -- helpers.js sits in the same directory
import { login } from './helpers.js';

export default function () {
  login();
}

go deeper

for a junior

Remember the one-line rule: in k6 you always write the file extension, and relative paths start with ./ or ../. If an import fails, read whether the message says the file was not found or the specifier was not recognised.

for a middle

Be able to explain the loader's branches: k6-prefixed names go to the built-in registry, dot or slash means a literal file path, :// means a URL, and everything else is refused. Say plainly that there is no node_modules lookup.

for a senior

Show that you diagnose from the exact error text, and that you know the fix for npm dependencies is a bundler that emits one self-contained file. Mention that this failure happens at load time, before any virtual user starts.

for a principal

The tradeoff is whether the team accepts a bundling step at all. Vendoring plain files keeps k6 runs dependency-free and debuggable; a bundler unlocks the npm ecosystem but adds a build artefact that must be versioned and kept in step with the tests.

## What k6 does with a module specifier Every `import` and every `require()` in a k6 script hands a string — the **module specifier** — to k6's own resolution step, whose only job is to turn that string into a module or a URL. In k6 v2 that step has exactly four outcomes, and only one of them ever touches your filesystem: - a specifier that is `k6` or begins with `k6/` goes to the built-in and extension module registry and never reaches disk at all; - a specifier whose first character is `.` or `/` (or a Windows drive letter such as `C:`) is treated as a **file path**; - a specifier containing `://` is treated as a **URL**, and only the `file` and `https` schemes are accepted; - anything else is refused outright with `The moduleSpecifier "lodash" couldn't be recognised as something k6 supports.` The file branch is deliberately unclever. k6 makes sure the importing module's URL ends in a slash, joins your specifier onto it as a URL, and reads that resulting path byte-for-byte off disk. There is no candidate list, no probing loop, no directory scan. That is what the k6 documentation means when it says k6 adopts **browser-like module resolution** and that file names for imports must be fully specified, such as `./helpers.js`. ## Why the missing suffix is fatal `./helpers` is a perfectly valid specifier, so the first stage succeeds: it resolves to something like `file:///srv/k6/tests/helpers`. The second stage then tries to read that exact path, finds nothing, and returns k6's local-disk error: ``` The moduleSpecifier "./helpers" couldn't be found on local disk. Make sure that you've specified the right path to the file. ``` This happens while k6 is loading the main module, before a single virtual user or iteration exists, so the run never starts. The message continues with a hint about mounting volumes when running in the Docker image, which is the other common cause of the same error. ## What Node does that k6 does not | Node.js resolution step | k6 v2 | |---|---| | Append `.js`, `.json`, `.node` to a path with no suffix | Never | | Fall back to `<dir>/index.js` when the path is a directory | Never | | Read `main` or `exports` from a nearby `package.json` | Never | | Walk up parent directories looking inside `node_modules` | Never | | Resolve a bare name such as `lodash` to an installed package | Never | There is no `node_modules` code path anywhere in k6, and no `package.json` is ever consulted. That is why installing a package with npm next to your script changes nothing: k6 cannot see it. The supported way to use an npm dependency is to run a bundler such as webpack or rollup and import the single self-contained file it emits. ## Two different failures that look alike Beginners often conflate two errors that come from different branches of the loader: 1. **`./helpers`** reaches the file branch and fails at read time, reporting `couldn't be found on local disk`. The fix is to write the suffix. 2. **`helpers`** or **`lodash`** never reaches the file branch, because the specifier does not start with a dot or a slash. It fails at resolve time, reporting `couldn't be recognised as something k6 supports`. The fix is to write a relative path, an absolute path, or an `https://` URL. Reading which of the two messages you got tells you immediately whether the problem is a typo in a path or a mistaken belief that k6 installs packages. ## Consequences for a shared helper file Say `smoke.js`, `load.js` and `soak.js` all pull in one `helpers.js`. Every one of the three has to spell the suffix out: - `import { login } from './helpers.js';` - `import { login } from '../lib/helpers.js';` - `import { login } from '/srv/k6/lib/helpers.js';` All three forms are legal. An absolute path works but ties the script to one machine's layout; k6 warns on Windows that an absolute import is not cross-platform and will not survive being moved or run with `k6 cloud`, and recommends the `file://` form if you insist on one. ## TypeScript does not relax the rule k6 transpiles with esbuild for files whose name ends in `.ts`, and that decision is made from the filename alone. A TypeScript helper is therefore imported as `./helpers.ts`. k6 will not try `.ts` and then `.js` for a bare `./helpers`, because there is still no probing anywhere in the loader — only the literal path you supplied.

  • Does k6 resolve a directory import such as './lib' to './lib/index.js'?
    No. k6 has no directory or index resolution at all, so `./lib` is treated as a filename and the read fails. Point the specifier at the file itself, for example `./lib/helpers.js`. The only specifiers k6 special-cases are `k6` and anything under `k6/`, which go to the built-in and extension registry instead of the filesystem.
  • What error does a bare specifier such as 'lodash' produce instead?
    A different one: `The moduleSpecifier "lodash" couldn't be recognised as something k6 supports.` The file branch is never entered, because k6 only treats a specifier as a path when it begins with a dot, a slash or a drive letter, and only treats it as remote when it contains `://`.
  • If npm installed the package next to my k6 script, why can k6 still not import it?
    Because k6 never looks in `node_modules` — there is no such lookup in the binary. To use an npm dependency you run a bundler such as webpack or rollup, which inlines the dependency into one self-contained file, and then import that emitted file by a relative path with its suffix written out.

A k6 import behaves like the src attribute on an HTML image tag: the exact path you typed is fetched, and nothing decides that you probably meant logo.png when you wrote logo. Node's resolver is a search; k6's is a literal fetch.

saying these in an interview costs you the question

  • k6 will find helpers.js if I just write ./helpers
  • npm install the package and k6 picks it up
  • an index.js in the folder makes ./lib importable
  • k6 runs on Node, so Node's resolver applies
  • a package.json main field tells k6 the entry file
open as a page

Why does importing k6/experimental/browser abort a k6 v2 run, and what replaces it?

level: juniorimportance: must knowfreq 71%

basics

~20 s

The browser module graduated to k6/browser, and k6 v2 keeps the old path registered only as a stub that throws a migration message. The run dies while the script is loading and k6 exits 107.

open as a page

Why does adding a new protocol to k6 require a compiled custom binary rather than a plugin file?

level: middleimportance: must knowfreq 72%

basics

~10 s

k6 extensions are Go code compiled into the k6 binary; k6 has no runtime plugin loader. Either k6 provisions a prebuilt binary for you automatically, or you build one yourself with xk6 build --with.

open as a page

Why does a k6 test script that imports Node's fs module fail before any VU runs?

level: middleimportance: must knowfreq 72%

basics

~20 s

k6 executes JavaScript on Sobek, a JavaScript engine written in Go, not on Node.js. It ships no Node standard library and no node_modules resolution, so a bare specifier like fs never resolves and loading the script fails during init.

open as a page

How does k6's own require() differ from an ES import statement in a test script?

level: middleimportance: must knowfreq 51%

basics

~20 s

k6 accepts both, but require() is k6's own implementation, not Node's: it loads built-in k6 modules, local files and remote https scripts only, it exists solely in the init stage, and it takes a specifier computed at runtime.

open as a page

In k6 v2, which module paths still live under k6/experimental/, and what does that prefix mean?

level: middleimportance: must knowfreq 62%

basics

~20 s

k6 v2 ships exactly three experimental modules: k6/experimental/csv, k6/experimental/fs and k6/experimental/streams. The prefix marks a module deliberately left outside k6's stability guarantee, so it may change in a minor release; every other importable path is core.

open as a page

What types of extension can a k6 binary carry, and how is each one invoked?

level: juniorimportance: should knowfreq 52%

basics

~10 s

k6 v2 recognises four extension types: JavaScript extensions imported as k6/x/ modules, output extensions selected with --out, secret source extensions selected with --secret-source, and subcommand extensions run as k6 x <name>.

open as a page

What does k6 do with a .ts test file, and what does it deliberately not do?

level: juniorimportance: should knowfreq 55%

basics

~20 s

k6 runs a .ts file directly by stripping its type annotations with esbuild, then executing the plain JavaScript. It never type-checks and never reads a tsconfig.json, so a wrong annotation costs nothing at run time.

open as a page

In k6 v2, what does --compatibility-mode=extended add on top of base?

level: middleimportance: should knowfreq 48%

basics

~20 s

Exactly one thing: extended defines global as an alias for globalThis, for the benefit of Node-oriented code. Base is plain Sobek. Extended is the default, and neither mode changes the ECMAScript level, TypeScript handling or module resolution.

open as a page

When a k6 helper module imports './config.js', which file's directory is that relative to?

level: middleimportance: should knowfreq 40%

basics

~20 s

The directory of the file the specifier is written in. k6 resolves a relative import against the importing module's own URL, never against the entry script, the command line argument, or the shell's working directory.

open as a page

In k6 v2, why does k6/experimental/websockets only warn while k6/experimental/grpc throws?

level: middleimportance: should knowfreq 48%

basics

~20 s

k6/experimental/websockets is deprecated, not removed: k6 v2 registers it as a wrapper that logs one notice and then hands back the same websockets implementation that k6/websockets exposes. k6/experimental/grpc is registered only to a throwing stub.

open as a page

When does k6's automatic extension resolution refuse to provision a binary, forcing an xk6 build?

level: seniorimportance: should knowfreq 48%

basics

~20 s

k6 v2 provisions binaries only from what the script declares — imports and use directives — and only for extensions its build service can build. Output extensions, private or in-development extensions, and offline runners all need xk6 build.

open as a page

In k6, which network imports are accepted, and how is a fetched module handled?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Only https is accepted; k6 refuses http and every other scheme for imports. It downloads the module at load time, executes it as your test's own code, resolves that module's relative imports against its https origin, and forbids it from reaching local files.

open as a page

How would you find and fix every dead k6 module import in scripts written for k6 v0.5x?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Scan the sources for k6/experimental/ rather than discovering the paths one failed run at a time, then classify each hit by hand. k6 v2's six removed paths lead to four different destinations, so a blanket rename is wrong for most of them.

open as a page

Should a team standardise k6 runs on automatic extension resolution or on a prebuilt custom binary?

level: principalimportance: should knowfreq 40%

basics

~10 s

Usually both, split by environment: automatic extension resolution locally for zero setup, and a pinned xk6-built binary in CI with K6_AUTO_EXTENSION_RESOLUTION=false, so runs never depend on a build service mid-pipeline.

open as a page

How do you pin which version of a k6/x extension a k6 script requires, and verify it?

level: middleimportance: nice to knowfreq 36%

basics

~20 s

Put a "use k6 with k6/x/faker >=v0.4.0" directive at the very top of the script file, or pin at build time with xk6 build --with module@version. Confirm with k6 deps, which prints each resolved constraint.

open as a page

Why can __ENV be populated under k6 run but empty under k6 archive or k6 cloud run?

level: seniorimportance: nice to knowfreq 38%

basics

~10 s

Because system environment variables reach __ENV by default only under k6 run. Every other k6 command defaults --include-system-env-vars to false, deliberately, so ambient secrets are not swept into an archive or a cloud upload.

open as a page

How would you distribute one shared k6 helpers.js across several test scripts and repositories?

level: principalimportance: nice to knowfreq 27%

basics

~20 s

k6 offers exactly three routes: a relative path inside one repository, a version-pinned https URL, or a bundler artefact imported as a local file. Since k6 has no node_modules lookup and no lockfile, the choice is really about how the version is pinned.

open as a page

Should a team standardise on a k6/experimental module, and how would you limit the blast radius?

level: principalimportance: nice to knowfreq 34%

basics

~20 s

Yes when no core k6 module does the job, no when one does. k6 excludes experimental paths from its stability allowlist, so contain the exposure: pin the k6 version and funnel the import through one shared local module.

open as a page