skip to content

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

level: seniorimportance: should knowfreq 48%

answer

  1. dependencies come from imports only
  2. a flag is not a dependency
  3. build service must know the extension
  4. K6_AUTO_EXTENSION_RESOLUTION=false disables it

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.

solid answer

~40 s

Automatic extension resolution is on by default in k6 v2. k6 collects the script's `k6/x/` imports and `use k6 with` directives, checks them against what the running binary contains, and if something is missing asks the k6 build service for a matching binary, caches it, and re-executes it as a subprocess. Everything it cannot do follows from that: `--out mysink` is a flag, not a dependency, so **output extensions are never provisioned**; an extension the build service does not know — yours, private, or patched — cannot be built remotely; and setting `K6_AUTO_EXTENSION_RESOLUTION=false`, or running without reach to the service, disables it entirely. In each case you fall back to `xk6 build --with`.

code

bash · 5 lines
bash
k6 deps --json script.js
# {"buildDependencies":{"k6":"*","k6/x/sql":"*"},
#  "imports":["k6/x/sql"],"customBuildRequired":true}

K6_AUTO_EXTENSION_RESOLUTION=false k6 run script.js

go deeper

for a junior

Know that a stock k6 binary can often run a script importing a k6/x/ module because k6 fetches a suitable binary for you, and that this behaviour can be switched off with an environment variable.

for a middle

Explain the pipeline: collect dependencies from imports and use directives, compare against the current binary, request a build, cache it, re-execute as a subprocess. The limits all follow from that first step.

for a senior

Predict the failure modes in a real pipeline — an output extension that never triggers provisioning, an air-gapped runner, a cold cache on a fresh container — and reach for k6 deps and a pinned xk6 build before the failure happens.

for a principal

Weigh a convenience that adds a per-run external dependency against a build you own. Decide where in the organisation that build lives, and what evidence a run must carry about which binary produced it.

## What automatic extension resolution actually does In k6 v2, `k6 run script.js` on a stock binary can still execute a script that imports a `k6/x/` module. The feature is **automatic extension resolution**, it is **on by default**, and it is controlled by the `K6_AUTO_EXTENSION_RESOLUTION` environment variable. The sequence is worth knowing precisely, because every limitation falls out of it: 1. k6 loads the main module. Imports it cannot resolve are collected instead of failing immediately. 2. It builds a **dependency set**: the unresolved modules, plus *every* `k6/x/` specifier the resolver touched, plus any `use k6 with …` directives found at the top of the script files, plus `k6` itself. 3. It compares that set against what the running binary contains — k6's own version plus the extensions compiled into it. 4. If the current binary satisfies everything, it just runs. Otherwise it asks the **k6 build service** (default `https://ingest.k6.io/builder/api/v1`, overridable with `K6_BUILD_SERVICE_URL`) for a binary matching those dependencies, caching the result on disk. 5. It **executes that binary as a subprocess**, passing the original arguments through, and propagates the subprocess's exit code as its own. Step 5 is the one people misread. k6 does not load code into itself and does not touch the `k6` on your `$PATH`; it launches a second process. The child is handed `K6_AUTO_EXTENSION_RESOLUTION=false` so it cannot recurse into provisioning again. ## Where it stops - **Output extensions are never provisioned.** The dependency set is built from *imports and directives only*. `--out mysink` is a flag value, and a flag value is not a script dependency, so k6 will simply report that the output type is invalid and list the ones it has. - **An extension the build service cannot build is out of reach.** Resolution serves published extensions the service knows about. Your own in-development extension, a private fork, or a patched version is not something a remote builder can produce for you. - **Turning the feature off returns you to strict behaviour.** With `K6_AUTO_EXTENSION_RESOLUTION=false`, an unresolvable `k6/x/` import ends the run before any VU starts, with a message naming the unknown modules and pointing at either resolution or a custom binary. - **It needs the network and credentials.** An air-gapped runner, a proxy that blocks the build service, or a cold cache on a fresh container all turn a "zero-setup" run into a failure or a first-run stall. ## When you must fall back to xk6 Each of the above resolves to the same fallback: build the binary yourself. ```bash xk6 build latest \ --with github.com/grafana/[email protected] \ --with github.com/<org>/xk6-output-<name> ./k6 run --out <name> script.js ``` That single command covers both an importable extension **and** an output extension, which is exactly the case automatic resolution cannot serve. Note the `./` — the new binary sits in the current directory and is not the `k6` on your path. ## The same four cases, side by side | situation | does resolution help? | why | |---|---|---| | script imports a published `k6/x/` module | **yes** | the import is a declared dependency the service can build | | `k6 x <name>` for a published subcommand | **yes** | the command name becomes a dependency of the same shape | | `--out <name>` for an output extension | **no** | a flag value is never collected as a dependency | | your own or a private extension | **no** | a remote build service cannot produce a module it cannot fetch | | runner with no route to the build service | **no** | provisioning is a network operation with an on-disk cache | ## Diagnosing which situation you are in - `k6 deps script.js` prints the resolved build dependencies, the imports it found, and a final **`Custom Build Required: yes/no`**. It reads imports and `use` directives only; it does not follow `require` calls. - `k6 deps --json script.js` gives the same as `buildDependencies`, `imports` and `customBuildRequired`, which is the form to assert on in a pipeline. - `k6 --version` shows what the binary in front of you already carries. ## The judgement to show Automatic resolution is a **convenience layer over the same compiled-binary model**, not a replacement for it. Treat it as excellent for local iteration on published extensions, and treat a pinned `xk6` build as the answer whenever the extension is yours, the destination is an output extension, or the environment cannot or should not reach a build service mid-run.

  • How does k6 run the binary it provisioned, and what happens to that binary's exit code?
    It launches the provisioned binary as a subprocess with the original command-line arguments, then propagates the child's exit code as its own, so a threshold breach or script error still surfaces normally. The child's environment has `K6_AUTO_EXTENSION_RESOLUTION=false` so it cannot provision again recursively.
  • Why can k6 not provision a binary for an output extension named on --out?
    The dependency set is assembled from what the script declares — unresolved imports, every `k6/x/` specifier seen, and `use k6 with` directives. A `--out` value never enters that set, so k6 evaluates it only when constructing outputs and reports an invalid output type, listing the ones it has. An output extension therefore always means a custom build.
  • What is the first thing to check when automatic resolution works locally but fails in CI?
    Reachability and cache state. The runner needs network access to the build service — the default endpoint, or whatever `K6_BUILD_SERVICE_URL` points at — and a fresh container starts with an empty binary cache, so the first run pays a build-and-download cost that a laptop with a warm cache never shows.

saying these in an interview costs you the question

  • Thinks --out mysink makes k6 fetch that output extension
  • Believes resolution can build your own unpublished extension
  • Assumes the provisioned binary replaces the k6 on PATH
  • Says resolution is purely local with no network call
  • Thinks disabling resolution just prints a warning