skip to content

Why does k6 reject a shared-iterations scenario configured with vus: 20 and iterations: 6?

level: middleimportance: should knowfreq 44%

answer

  1. one of the two numbers is impossible
  2. a pool smaller than its consumers
  3. caught before any VU starts
  4. the sibling executor accepts it

basics

~10 s

shared-iterations requires iterations to be at least vus, so a pool of 6 leaves 14 of the 20 VUs nothing to claim. k6 fails configuration validation before the run starts and names both numbers.

solid answer

~40 s

`shared-iterations` treats `iterations` as one pool shared by all VUs, and k6 starts every configured VU up front, so a pool smaller than the VU count would initialise 20 VUs for 6 units of work. k6 refuses it during configuration validation — before any VU exists — with *the number of iterations (6) can't be less than the number of VUs (20)*, and leaves its invalid-config exit status rather than running a truncated test. `per-vu-iterations` has no such rule, because there `iterations` is a per-VU count: the same numbers are valid and mean 120 iterations. Both executors additionally require `vus` above 0 and a `maxDuration` of at least one second.

code

javascript · 9 lines
javascript
export const options = {
  scenarios: {
    // Rejected: the shared pool is smaller than the VU count.
    bad: { executor: 'shared-iterations', vus: 20, iterations: 6 },

    // Accepted: 6 iterations for each VU, 120 in total.
    good: { executor: 'per-vu-iterations', vus: 20, iterations: 6 },
  },
};

go deeper

for a junior

Recall the rule itself: on shared-iterations the iterations value must be at least the vus value. If you shrink the budget while debugging, shrink the VU count with it or the run will not start.

for a middle

Explain why the rule exists — the pool is shared and every configured VU is started up front, so extra VUs would have nothing to claim — and that per-vu-iterations has no equivalent rule because its count is private per VU.

for a senior

Read the message as a signal about intent: twenty VUs each doing six passes is a per-vu-iterations request written against the wrong executor. Note that failing at configuration time is preferable to running a test whose shape nobody intended.

for a principal

The angle is where such constraints get caught. A config that only fails on a real runner wastes a pipeline slot, so argue for validating scenario configuration in a cheap early step rather than discovering it at the head of a long run.

## The rule k6 enforces A `shared-iterations` scenario must be configured with `iterations` **greater than or equal to** `vus`. With `vus: 20` and `iterations: 6` k6 refuses the configuration before the run starts and reports: ``` There were problems with the specified script configuration: - the number of iterations (6) can't be less than the number of VUs (20) ``` This is a configuration-validation failure, not a runtime one. It happens while k6 consolidates options, after the script has been parsed but before a single VU is initialised, and it leaves k6's invalid-config exit status (104) rather than running a shortened test. ## Why `shared-iterations` has the rule The executor's model is a single pool: `iterations` is the total for the scenario, and every VU claims the next number from one shared counter as it becomes free. k6 also starts **all** `vus` VUs up front — the executor reserves them for the whole scenario. So `vus: 20, iterations: 6` asks k6 to initialise twenty VUs for a pool of six units of work. Fourteen of them would have nothing to claim and would idle for the scenario's whole life while still consuming memory and a runtime each. Rather than accept a configuration that can only mean a mistake, k6 rejects it and names both numbers so the fix is obvious. ## What `per-vu-iterations` checks instead The sibling executor has no relationship between the two keys at all, because `iterations` there is a **per-VU** count, not a pool. Every VU gets its own loop of that length, so `vus: 20, iterations: 6` is perfectly valid and runs `20 * 6 = 120` iterations. | configuration | `shared-iterations` | `per-vu-iterations` | |---|---|---| | `vus: 20, iterations: 6` | rejected — budget below VU count | valid, 120 iterations total | | `vus: 20, iterations: 500` | valid, 500 iterations total | valid, 10,000 iterations total | | `vus: 0` | rejected | rejected | | `iterations: 0` | rejected, since 0 is below any positive `vus` | rejected outright | | `maxDuration: '500ms'` | rejected | rejected | ## The checks both executors share Every iteration-budget scenario is validated against the same short list, and any failure is reported the same way: - `vus` must be **more than 0** — *the number of VUs must be more than 0*. - `maxDuration` must be **at least one second** — a smaller value is rejected outright, not rounded up. - The scenario name may contain only digits, latin letters, underscores and dashes. - `startTime` and `gracefulStop`, both shared by every executor, may not be negative. - An `exec` naming a function the script does not export fails too, with *function '...' not found in exports*. All of the messages for one run are collected and printed together under a single `There were problems with the specified script configuration:` heading, so you fix the whole set in one edit rather than one error per attempt. The collection is across scenarios too: an options object holding four scenarios reports every offending one at once, which matters when the configuration is assembled from a shared module and you cannot see all of it in a single file. ## Where it bites in practice: the 500-row CSV pass The rule almost never fires on the configuration you intended; it fires when you have edited one number and not the other. Two realistic ways to trip it on a 500-row data-driven pass: 1. You shrink the budget while debugging — `iterations: 500` becomes `iterations: 5` so the run finishes fast — but leave `vus: 20` in place. k6 now refuses to start at all, which is a better outcome than quietly running five of your twenty VUs. 2. You raise concurrency to make a long pass fit inside `maxDuration`, pushing `vus` past a budget you had already trimmed. The same error appears, and the fix is to raise `iterations` back or lower `vus`. 3. You parameterise the budget from an environment variable so the same script can run a smoke pass and a full pass, and the smoke value drops below the VU count that was left hard-coded. The run refuses to start, which is the failure you want rather than a smoke pass that quietly exercised a handful of rows. The takeaway for reading the message: it is telling you the two numbers disagree about **what kind of budget you meant**. If you wanted twenty VUs each doing six passes, you did not want `shared-iterations` at all — you wanted `per-vu-iterations`, where those exact numbers are legal and mean 120 iterations.

  • Does k6 catch that at parse time, at start-up, or once the run is under way?
    During configuration consolidation — after the script is parsed but before any VU is initialised. All configuration problems for the run are collected and printed together under *There were problems with the specified script configuration:*, so you fix the whole set in one edit.
  • What other values will k6 reject on an iteration-budget scenario?
    A `vus` of 0 or less on either executor; an `iterations` of 0 or less on `per-vu-iterations`; a `maxDuration` under one second on both; a negative `startTime` or `gracefulStop`; a scenario name using anything but digits, letters, underscores and dashes; and an `exec` naming a function the script does not export.

saying these in an interview costs you the question

  • Thinks iterations must be an exact multiple of vus
  • Expects k6 to silently lower vus to match the budget
  • Believes k6 discovers the problem only once the run starts
  • Says per-vu-iterations enforces the same iterations-versus-vus rule
  • Assumes the run proceeds with fourteen VUs simply sitting idle