skip to content

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

level: principalimportance: should knowfreq 40%

answer

  1. two routes to the same binary
  2. convenience versus an owned artifact
  3. split the default by environment
  4. gate on customBuildRequired

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.

solid answer

~40 s

There is no single answer, so argue the tradeoff. Automatic resolution costs nothing to adopt — a stock k6 runs the script, constraints live next to the code, and a bump is a one-line `use k6 with` edit — but every run then depends on reaching the build service, a fresh runner starts with a cold cache, and it cannot serve output extensions or your own unpublished ones. A prebuilt `xk6` binary is exact and works offline, at the cost of an artifact someone must own, rebuild and distribute. A defensible position in k6 v2 is resolution on locally, a pinned binary plus `K6_AUTO_EXTENSION_RESOLUTION=false` in CI, and `k6 deps --json` gating the pipeline either way.

code

bash · 4 lines
bash
export K6_AUTO_EXTENSION_RESOLUTION=false
k6 deps --json script.js > deps.json
./k6-extended run script.js
./k6-extended --version

go deeper

for a junior

Know that the same k6 test can run from a stock binary that fetches what it needs, or from a binary someone built in advance, and that the team usually agrees on one default per environment.

for a middle

Be able to say what each route needs at run time: resolution needs the build service and a cache, a prebuilt binary needs distribution. Name K6_AUTO_EXTENSION_RESOLUTION as the switch between them.

for a senior

Argue from failure modes you have seen — cold caches on ephemeral runners, a blocked build endpoint during an incident, an output extension that resolution silently cannot serve — and propose a gate rather than a preference.

for a principal

Own the ambiguity risk. Decide the default per environment, make the choice observable in run output, and name who rebuilds the artifact when k6 or an extension releases; the cost of this decision is paid over years.

## The decision, stated plainly k6 v2 offers two ways to end up holding a binary that contains an extension, and a team has to pick a default: - **Automatic extension resolution** — leave `K6_AUTO_EXTENSION_RESOLUTION` at its default of true. Anyone runs `k6 run script.js` with a stock k6; k6 works out the dependencies from the script's `k6/x/` imports and `use k6 with` directives, asks the build service for a matching binary, caches it, and re-executes it as a subprocess. - **A prebuilt custom binary** — someone runs `xk6 build --with …@version` once, and every developer and runner uses that artifact. Neither is universally right, which is why this is asked as a judgement question rather than a factual one. ## What each buys and costs | | automatic resolution | prebuilt xk6 binary | |---|---|---| | **setup for a newcomer** | none — a stock k6 works | must obtain the artifact first | | **reproducibility** | as strong as the constraints in the script | exact, one artifact | | **network at run time** | needs the build service unless cached | none | | **your own extension** | not possible | the only route | | **output extensions** | never provisioned | supported | | **upgrade path** | change a constraint | rebuild and redistribute | | **who owns it** | nobody, until it breaks | a named owner | ## The argument for automatic resolution as the default It removes the single most common friction in adopting an extension: the colleague who cannot run your test. Constraints live next to the code that needs them, review sees them in the diff, and a version bump is a one-line change to a `use k6 with` directive. For published extensions and day-to-day local iteration, it is hard to justify anything heavier. ## The argument for a prebuilt binary in the pipeline 1. **Runtime independence.** A build service reachable from a laptop is not necessarily reachable from a hardened runner, and a run that fails because a *build* endpoint was unavailable is a confusing failure to debug during an incident. 2. **Exactness.** A wildcard constraint means "whatever satisfies it today". A pinned artifact means the same bytes ran last week and this week, and that the extension version in the artifact is the one someone actually chose. 3. **Cold caches.** A fresh container starts with an empty binary cache, so the convenience that costs nothing on a warm laptop costs a build-and-download on every ephemeral runner. 4. **Coverage.** Output extensions and unpublished or private extensions are simply outside what resolution can serve, so a team that needs either will end up building anyway. ## A defensible position to argue Split by environment rather than picking one globally: - **Local development:** automatic resolution on. Optimise for the newcomer. - **CI and scheduled runs:** a prebuilt binary, with `K6_AUTO_EXTENSION_RESOLUTION=false` set so a missing extension fails loudly instead of quietly provisioning something else. - **Both:** keep `use k6 with` directives in the scripts regardless. They document intent, they are what a reviewer sees, and they let `k6 deps --json` be a gate — fail the pipeline when `customBuildRequired` is true for the binary the pipeline holds. Then name the things you still own: who rebuilds when k6 or an extension releases, where the artifact lives, and how a run records which binary produced it. `k6 --version` printing the `Extensions:` block is the cheapest such record, and worth capturing in the job log. ## How to move a team without a flag day 1. **Add the directives first.** `use k6 with` lines cost nothing under either route and make the requirement reviewable. Until they exist, no gate you build can mean anything. 2. **Measure before switching.** Run `k6 deps --json` over the existing scripts and look at `customBuildRequired`; that tells you how many tests actually depend on an extension, which is usually fewer than people assume. 3. **Build the artifact and run both.** Keep resolution enabled while the prebuilt binary runs alongside, and compare. A mismatch here is far cheaper to find than during an incident. 4. **Flip the switch last.** Only once the artifact is produced automatically and distributed does `K6_AUTO_EXTENSION_RESOLUTION=false` belong in CI, and by then it is a formality rather than a change of behaviour. ## The trap to avoid The worst outcome is *accidental* mixing: some runners provisioning, some using a prebuilt binary, and nobody able to say which one produced a given result. Whichever default you choose, make it explicit and observable — the failure mode of this decision is ambiguity, not slowness.

  • How would you make a k6 pipeline fail loudly when its binary lacks a required extension?
    Set `K6_AUTO_EXTENSION_RESOLUTION=false` so nothing is provisioned behind your back, and run `k6 deps --json script.js` as a gate: `customBuildRequired` is true exactly when the binary you invoked cannot satisfy the script. That turns a silent substitution into an explicit build failure before any VU starts.
  • What should a run record about the binary that produced it?
    At minimum the output of `k6 --version`, which prints an `Extensions:` block listing each compiled-in extension's module path, version, registered name and type. Captured in the job log it answers, months later, exactly which artifact produced a result — the question that becomes unanswerable once some runners provision and others do not.
  • When is a prebuilt binary not optional?
    Whenever the capability is outside what resolution can serve: an output extension, since a `--out` value is never treated as a script dependency; an extension you are still developing; a private or patched fork; or an environment with no route to a build service. In those cases `xk6 build --with` is the only route.

saying these in an interview costs you the question

  • Declares one option always correct without naming a tradeoff
  • Ignores that a fresh runner starts with an empty binary cache
  • Assumes resolution can serve private or output extensions
  • Lets some runners provision while others use a prebuilt binary
  • Treats a wildcard constraint as a version pin