skip to content

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

level: juniorimportance: should knowfreq 52%

answer

  1. four types, four different entry points
  2. import, flag, flag, command
  3. only JS extensions use k6/x/
  4. subcommands live under k6 x

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>.

solid answer

~30 s

k6 has one extension registry with four types. A **JavaScript extension** registers a module name that must begin with `k6/x/` and is reached by importing it in the script. An **output extension** registers a name that becomes valid for `k6 run --out <name>` and is never imported. A **secret source extension** registers a type for the `--secret-source <type>=<config>` flag. A **subcommand extension** adds a CLI command under the reserved `k6 x` namespace, so you run `k6 x mytool`. Only the JavaScript type carries the `k6/x/` prefix; the other three register plain names.

code

bash · 4 lines
bash
k6 run script.js                      # imports k6/x/mqtt
k6 run --out mysink script.js         # output extension
k6 --secret-source file=./secrets.txt run script.js
k6 x mytool --help                    # subcommand extension

go deeper

for a junior

Learn the four names and their entry points: import for JavaScript, --out for output, --secret-source for secrets, k6 x for subcommands. Being able to list them is most of what is asked here.

for a middle

Explain that all four register into one registry during Go package initialisation, that only JavaScript names carry the k6/x/ prefix, and that a duplicate registered name is a startup panic rather than a silent override.

for a senior

Use the type to predict behaviour: an import can trigger automatic provisioning while an --out value cannot, and a subcommand extension gets read-only access to k6's global state, which bounds what such a tool may safely do.

for a principal

Decide which surface a capability belongs on. The same feature exposed as a subcommand, an output or an importable module lands on different people and different review paths, and that choice outlives the code.

## The four kinds of extension Every k6 extension registers itself into one process-wide registry, and that registry recognises **exactly four extension types**. The type decides which surface of k6 the extension widens and, crucially, **how a user reaches it** — none of the four is reached the same way as another. | type | registered with | how a user invokes it | naming rule | |---|---|---|---| | **JavaScript** | `modules.Register` | `import x from 'k6/x/name'` in the script | name **must** start with `k6/x/` | | **output** | `output.RegisterExtension` | `k6 run --out name script.js` | plain name, no prefix | | **secret source** | `secretsource.RegisterExtension` | `k6 --secret-source name=<config>` | plain name, no prefix | | **subcommand** | `subcommand.RegisterExtension` | `k6 x name [args…]` | plain name, must match the command's own name | All four registration calls funnel into the same underlying `ext.Register`, which is why the guarantees are identical across types: registration happens during Go package initialisation, and a **duplicate name within one type panics** rather than letting the last one win. ## JavaScript extensions These are the ones people mean by default. They add importable modules — a client for a protocol k6 does not speak, a data generator, a faster implementation of something awkward in JS. The `k6/x/` prefix is not a convention: `modules.Register` **panics** if the name lacks it, so a mis-named extension takes the binary down at startup instead of failing mysteriously later. The prefix also tells a reader of the script instantly that the import cannot be satisfied by a stock k6 binary. ## Output extensions An output extension registers a name that becomes valid for the `--out` flag. It is **not** imported by the script, which surprises people: nothing in the JavaScript changes when you add one. A name that collides with a built-in output is rejected outright when k6 assembles its output list, so an extension cannot quietly shadow a built-in. ## Secret source extensions A secret source registers a type name that becomes valid for the `--secret-source` flag, whose value has the shape `type=config`. k6 resolves the named source at startup and the values it hands back are cached and redacted, so the extension's only job is retrieval. ## Subcommand extensions A subcommand extension adds a **CLI command, not test behaviour**. It lives under the reserved `k6 x` namespace — `k6 x mytool` — and never becomes a top-level `k6` command, which keeps the core command set stable. Its constructor receives k6's global state read-only, so it can see configuration, the logger and the filesystem without being able to disturb the runtime. ## How registration actually happens All four registration calls are made from a Go `init()` function, so they run when the binary starts, before any command is parsed. That has three consequences worth stating: - **The set of extensions is fixed for the life of the process.** Nothing is discovered, scanned for, or loaded later; `k6 --version` can therefore print a complete `Extensions:` block naming each entry's module path, version, registered name and type. - **Collisions are fatal, not resolved.** Registering a name twice for the same type panics during startup. That is deliberate — a silent winner would make the behaviour of a binary depend on package initialisation order. - **k6's own components use the same door.** The built-in `file` and `url` secret sources register through `secretsource.RegisterExtension` exactly as a third-party one would, which is why `--secret-source file=./secrets.txt` and a secret source you added look identical on the command line. ## Why the distinction matters in practice 1. **It tells you what to change.** Needing a protocol means a JavaScript extension and an edited script; needing a metrics destination means an output extension and an edited command line. 2. **It tells you what automatic resolution will do for you.** In k6 v2, importing a `k6/x/` module or invoking `k6 x <name>` can trigger provisioning of a suitable binary; selecting an output with `--out` cannot, because a flag value is never treated as a script dependency. 3. **It tells you what to look for in `k6 --version`.** The `Extensions:` block prints each entry's type, so you can see at a glance whether the binary carries the *kind* of extension you need. A useful sanity check when someone describes "a k6 extension": ask how it is invoked. If the answer is an `import`, it is a JavaScript extension; if it is a flag, it is an output or a secret source; if it is a command, it is a subcommand. There is no fifth answer.

  • Why does adding an output extension require no change to the k6 script?
    An output extension registers a name for the `--out` flag, not a module name. Nothing in the JavaScript refers to it, so the script stays byte-identical and the choice of destination lives entirely on the command line. That also means k6 cannot infer the need for one from the script's imports.
  • What happens if a k6 output extension registers a name that a built-in output already uses?
    k6 refuses to assemble its output list and reports that a built-in output with that type already exists. The extension cannot shadow a built-in, so `--out json` always means k6's own JSON output. The fix is to rename the extension's registered output.

saying these in an interview costs you the question

  • Thinks k6 extensions are only importable JavaScript modules
  • Believes an output extension must be imported in the script
  • Expects a subcommand extension as a top-level k6 command
  • Assumes every extension type needs the k6/x/ prefix
  • Says a secret source is configured in the options object