skip to content

In a k6 arrival-rate scenario, what do preAllocatedVUs and maxVUs control, and what happens when maxVUs is omitted?

level: juniorimportance: must knowfreq 74%

answer

  1. two numbers, one required
  2. one is built before the clock starts
  3. the other is only a ceiling
  4. unset ceiling copies the floor
  5. arrival-rate executors only

basics

~10 s

preAllocatedVUs is the number of VUs k6 initializes before an arrival-rate scenario starts; maxVUs is the hard ceiling that pool may grow to mid-run. Omitted, maxVUs equals preAllocatedVUs, so the pool never grows.

solid answer

~40 s

In k6 v2 the two arrival-rate executors, `constant-arrival-rate` and `ramping-arrival-rate`, both take `preAllocatedVUs` and `maxVUs`; no other executor has them. `preAllocatedVUs` is **required** and is the number of VUs k6 constructs during its `Init VUs...` phase, before the scenario clock starts. `maxVUs` is optional and is an absolute ceiling: the difference `maxVUs - preAllocatedVUs` is how many extra VUs k6 may initialize lazily while the run is in flight. If you leave `maxVUs` out, k6 sets it equal to `preAllocatedVUs`, so the pool is fixed at whatever you preallocated. Writing `maxVUs` lower than `preAllocatedVUs` is a config error (`maxVUs can't be less than preAllocatedVUs`) and k6 refuses to start the test.

code

javascript · 18 lines
javascript
import http from 'k6/http';

export const options = {
  scenarios: {
    steady: {
      executor: 'constant-arrival-rate',
      rate: 100,
      timeUnit: '1s',
      duration: '1m',
      preAllocatedVUs: 40,
      maxVUs: 60,
    },
  },
};

export default function () {
  http.get('https://test.k6.io/');
}

go deeper

for a junior

Remember the two names and which is mandatory: every arrival-rate scenario must carry preAllocatedVUs, and maxVUs is optional. If you leave maxVUs out, the pool stays exactly the size you preallocated.

for a middle

Explain the mechanics: k6 constructs preAllocatedVUs VUs during its init phase, and the difference up to maxVUs is a budget of extra VUs it may build lazily while the run is already going.

for a senior

Show that you treat the ceiling as an emergency valve, not a plan. Say what you would look at after a run to decide whether the preallocated number was right, and why growing mid-run is a poor substitute.

for a principal

Frame it as a generator-provisioning decision: the preallocated count fixes the memory and startup cost of the run before it begins, and standardising that number across a suite is what makes runs cheap to schedule and compare.

## The two keys A k6 VU (virtual user) is a JavaScript runtime with its own copy of the script's per-VU state. In k6 v2, an arrival-rate scenario starts iterations on a clock rather than as VUs become free, so it needs a supply of VUs to hand those starts to. Two options size that supply: - **`preAllocatedVUs`** - how many VUs k6 constructs *before* the scenario begins. It is **required**; omit it and k6 reports `the number of preAllocatedVUs isn't specified` and never starts. - **`maxVUs`** - the absolute ceiling on how many VUs the scenario may ever hold. It is optional. The gap between them, `maxVUs - preAllocatedVUs`, is k6's internal budget of *unplanned* VUs: VUs it is allowed to build on demand once the run is already going. ## Where the keys are legal Only the two arrival-rate executors declare them. The VU-count and iteration-count executors have neither, so pasting `preAllocatedVUs` into a `constant-vus` scenario is an unknown key inside a `scenarios` entry - fatal, not ignored. | key | `constant-arrival-rate` | `ramping-arrival-rate` | |---|---|---| | `preAllocatedVUs` | required | required | | `maxVUs` | optional, defaults to `preAllocatedVUs` | optional, defaults to `preAllocatedVUs` | | rate input | `rate` | `startRate` | | shape input | `duration` | `stages` (each `target` is a rate) | | `timeUnit` | optional, default `"1s"` | optional, default `"1s"` | ## What k6 does at start-up 1. It parses and validates every scenario. Validation failures here stop the run before a single VU exists. 2. It enters its `Init VUs...` phase and constructs exactly `preAllocatedVUs` VUs, running the script's init context once per VU. The progress line counts them as `n/m VUs initialized`. 3. It initializes the executors and starts the scenario clock. 4. From then on it hands scheduled iteration starts to whichever VU in the pool is idle. Nothing in the `maxVUs` headroom is built during step 2. Those VUs exist only as permission to build more later. ## Omitting, or mis-ordering, the two values - **`maxVUs` unset** - k6 copies `preAllocatedVUs` into it during validation. The pool is then fixed: it can never exceed what you preallocated. - **`maxVUs` below `preAllocatedVUs`** - validation fails with `maxVUs can't be less than preAllocatedVUs`. k6 does not clamp it or pick the larger of the two. - **`maxVUs` equal to `preAllocatedVUs`** - legal, and identical in behaviour to leaving it out. - **`preAllocatedVUs` negative** - validation fails with `the number of preAllocatedVUs can't be negative`. - **`preAllocatedVUs: 0` with no `maxVUs`** - passes validation, but the scenario has no VUs and therefore no work to do. ## What the headroom actually buys When a scheduled start finds every VU busy and headroom remains, k6 asks a background goroutine to build one more VU and moves on. That build is non-blocking and one-at-a-time, so growth is gradual rather than instantaneous. Because k6 logs the build at debug level with the note *this may affect test results*, the headroom is best understood as an emergency valve rather than a sizing strategy: `preAllocatedVUs` is the number you are meant to get right. ## Reading the pool back out of the run k6 prints a one-line description of every scenario before it starts, and the pool shows up there. The label is always `maxVUs`, whichever way you configured it: - with `preAllocatedVUs: 40` and `maxVUs: 60` the line reads `maxVUs: 40-60` - the range k6 is allowed to work in; - with `maxVUs` unset, or set equal to the floor, it collapses to `maxVUs: 40` - a single number, and your cue that the pool is fixed. The same line carries the scenario's other facts, such as `gracefulStop: 30s`, in the same parenthesised list. During the run, the progress line for the scenario shows two counts against the pool: how many VUs are executing an iteration right now, and how many exist at all. ## A worked run ```javascript export const options = { scenarios: { steady: { executor: 'constant-arrival-rate', rate: 100, timeUnit: '1s', duration: '1m', preAllocatedVUs: 40, maxVUs: 60, }, }, }; ``` k6 builds 40 VUs before the clock starts and may build up to 20 more during the minute. If every iteration finishes in well under 400 ms, the 40 are enough and the extra 20 are never created. If iterations get slower and the 40 are all busy at a start time, that start is lost and k6 begins building VU 41. Delete the `maxVUs` line and the same scenario is capped at 40 for its whole duration - the only signal that the pool ran dry would then be the `dropped_iterations` counter and k6's `Insufficient VUs` warning.

  • Can you put preAllocatedVUs on a k6 constant-vus scenario to warm the pool?
    No. Only `constant-arrival-rate` and `ramping-arrival-rate` declare `preAllocatedVUs` and `maxVUs`. `constant-vus` takes `vus` and `duration`, and an unrecognised key inside a `scenarios` entry is fatal - k6 fails config parsing rather than ignoring it.
  • Does k6 initialize the maxVUs headroom before the run starts?
    No. k6's `Init VUs...` phase builds exactly `preAllocatedVUs` VUs. The headroom `maxVUs - preAllocatedVUs` is only permission to build more later; each of those is constructed during the run, one at a time, in the background.
  • How are preAllocatedVUs and maxVUs interpreted when a k6 run is split across execution segments?
    Both numbers are scaled to the segment the instance owns, so each instance preallocates only its share of the pool and enforces its share of the ceiling. The values you write in `options` stay whole-test numbers.

saying these in an interview costs you the question

  • Thinks maxVUs defaults to unlimited when it is left out
  • Believes k6 preallocates maxVUs worth of VUs before the run
  • Assumes k6 silently raises maxVUs when it is below preAllocatedVUs
  • Puts preAllocatedVUs on a constant-vus or shared-iterations scenario
  • Treats preAllocatedVUs as optional because maxVUs is set