skip to content

What happens when a k6 `options.scenarios` entry carries a key that its executor does not define?

level: seniorimportance: nice to knowfreq 38%

answer

  1. strictness is not uniform
  2. the level decides the outcome
  3. one warns, the other stops
  4. unknown fields disallowed per entry

basics

~20 s

It is fatal. k6 decodes each scenario entry with unknown fields disallowed, so a key that is neither a shared scenario key nor one of the named executor's own keys fails config parsing and exits 104 before any traffic is sent.

solid answer

~50 s

k6 parses a scenario entry in two passes: it reads the entry's `executor` string, then hands the entry's raw JSON to that executor's own config constructor, which decodes strictly with unknown fields disallowed. A key the executor does not define — a typo like `graceful_stop`, a key belonging to a different executor, or your own annotation such as `owner` — is therefore an error, not a warning, and k6 exits 104 without generating load. This is deliberately stricter than the root of `options`, where an unrecognised key only produces the log line *There were unknown fields in the options exported in the script* and the run continues. The asymmetry is worth remembering because it makes the failure modes look nothing alike: a stray root option quietly does nothing, a stray scenario key stops the run.

code

javascript · 14 lines
javascript
export const options = {
  // Unrecognised here: k6 logs a warning and the run continues.
  vusss: 10,

  scenarios: {
    load: {
      executor: 'constant-vus',
      vus: 10,
      duration: '30s',
      // Unrecognised here: config parsing fails and k6 exits 104.
      graceful_stop: '5s',
    },
  },
};

go deeper

for a junior

Remember that a scenario entry only accepts the shared keys plus the ones its own executor defines, and that anything else stops the run rather than being ignored.

for a middle

Explain the asymmetry precisely: unknown at the root of options is a logged warning and the run proceeds, unknown inside a scenario entry is invalid configuration and exit 104.

for a senior

Trace why the levels differ — the entry is decoded by the executor's constructor with unknown fields disallowed, so no lenient retry can rescue it — and read a pipeline's 104 as a named field rather than a flake.

for a principal

Consider what generates your scenario maps. Any layer that templates or injects fields into an entry inherits this strictness, so metadata belongs outside the object rather than inside it.

## Two levels, two behaviours k6 checks the shape of your `options` object twice, and it treats the two levels differently. Stating the rule flatly in either direction gets it wrong: | where the unrecognised key sits | what k6 does | does the run start? | |---|---|---| | the root of the exported `options` object | logs *There were unknown fields in the options exported in the script* | yes | | inside an entry of the `scenarios` map | fails configuration parsing and exits **104** | no | So `export const options = { vusss: 10, duration: '30s' }` runs — with one VU, because the misspelled key configured nothing. Move the same class of mistake one level down, into a scenario entry, and the run never starts. ## Why the levels differ The root object is decoded with unknown fields disallowed and then, if that fails, decoded again leniently; when the lenient pass succeeds, k6 downgrades the problem to a warning and carries on. A scenario entry cannot take that escape route: 1. k6 reads the `scenarios` map and, for each entry, extracts only the `executor` string, keeping the rest of the entry as raw JSON. 2. It looks up the constructor registered for that executor type; an unregistered name fails here with `unknown executor type '<value>'`. 3. That constructor decodes the raw JSON into the executor's own config struct **with unknown fields disallowed**, and returns an error if any key does not belong. Because step 3 raises a real error rather than merely refusing a lenient decode, it fails on the second pass exactly as it did on the first — so the warning path is never reached and the error surfaces as invalid configuration. ## What you actually see The run stops during configuration, before VU initialisation, with a message naming the offending field and an exit code of `104`. Nothing is sent to the system under test, no metrics are produced, and no summary is written. In a pipeline this reads as an immediate red rather than a run with suspicious numbers — which is the useful behaviour, even though it is the harsher one. It is worth knowing which neighbours share that exit code, because they all fail at the same moment and look alike in a log: a scenario entry with no `executor`, an `executor` naming a type k6 does not register, an `exec` naming a function the script never exports, a negative `startTime` or `gracefulStop`, and a root-level shortcut such as `duration` written alongside an explicit `scenarios` map. Every one of them is settled before a single VU exists, so none of them can waste a long run's worth of time. The corollary is that k6 configuration failures are cheap to iterate on: the feedback loop for a malformed scenario map is a second or two, not the length of the test you were trying to run. ## Where this bites in practice - **Case and separator slips.** `graceful_stop` is not `gracefulStop`, and k6 will not guess. - **Keys borrowed from another executor.** Each executor defines its own set on top of the shared block, so a key that is valid in one entry can be fatal in the one next to it. - **Options copied from older material.** k6 v2.0.0 removed identifiers that older examples still show, including the `externally-controlled` executor, and pasting one in fails at step 2 rather than step 3. - **Hand-written annotations.** You cannot document an entry by adding your own `description` or `owner` key; strict decoding rejects it. Put the note in a JavaScript comment above the entry instead. - **Generated configuration.** A templating or config layer that injects metadata alongside the real keys will break the run rather than being ignored. ## The habits that avoid it 1. Build a scenario entry by starting from the shared keys — `executor`, `startTime`, `gracefulStop`, `exec`, `env`, `tags` — and then adding only keys documented for the executor you named. 2. When you change a scenario's `executor`, re-check the whole entry rather than the new keys alone; keys left behind from the previous executor are now unknown ones. 3. Keep commentary in comments, not in the object. 4. Treat exit 104 in a pipeline as a configuration bug to read, not as a flaky run to retry — it is deterministic and it names the field. ## The one thing not to over-generalise It is tempting to compress all this into "an unknown option means exit 104". That is wrong at the root of `options`, where the run continues after a warning, and a candidate who states it flatly is usually reciting rather than remembering. The precise claim is the one worth carrying: **strict inside a scenario entry, forgiving at the root.**

  • Why can k6 not simply warn about an unknown key inside a scenario entry as well?
    Because the entry is decoded by the executor's own config constructor, which returns an error rather than declining to decode. That error propagates through both the strict and the lenient parse of the options object, so there is no successful decode left for k6 to fall back to and downgrade into a warning.
  • Does a stray key inside a k6 scenario entry fail before or after VUs are initialised?
    Before. Configuration is parsed and validated as the script bundle is built, ahead of VU initialisation, `setup()` and any traffic. That is also true of a missing `executor`, an unknown executor type and an `exec` naming a function the script does not export — all of them are exit 104 at config time.

saying these in an interview costs you the question

  • Saying any unknown k6 option always exits 104
  • Saying k6 always ignores keys it does not recognise
  • Adding a description or owner key to document a scenario entry
  • Retrying an exit 104 run as though it were flaky
  • Changing a scenario's executor without revisiting its other keys