How do you pick preAllocatedVUs for a k6 constant-arrival-rate scenario targeting 500 iterations per second?
answer
- duration times rate, plus slack
- measure the median, do not guess
- sleep counts against the pool
- size a ramp off its peak target
- confirm with dropped_iterations at zero
basics
~20 sUse k6's rule of thumb: preAllocatedVUs is roughly the median iteration duration times the rate, plus headroom. At 500 iterations per second with a 200 ms iteration that is about 100 VUs; then tune until dropped_iterations stays at zero.
solid answer
~40 sk6's documentation gives the starting formula `preAllocatedVUs = [median_iteration_duration * rate] + constant_for_variance`. With `rate: 500, timeUnit: '1s'` and a median iteration of 200 ms, the arithmetic gives 100, and you would write something above that - say 130 - because iteration duration is not constant. The duration that matters is what k6 reports as `iteration_duration`: the whole `default` function, including any `sleep()`. That is why k6's docs tell you not to `sleep()` at the end of an arrival-rate iteration - `rate` and `timeUnit` already pace the starts, and the sleep only inflates the number of VUs you must preallocate. The first number is always provisional: run it, read `dropped_iterations`, raise `preAllocatedVUs`, repeat.
code
javascript · 21 linesimport http from 'k6/http';
// 500 iters/s, measured median iteration_duration ~200ms -> 100, plus slack
export const options = {
scenarios: {
peak: {
executor: 'constant-arrival-rate',
rate: 500,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 130,
},
},
thresholds: {
dropped_iterations: ['count===0'],
},
};
export default function () {
http.get('https://test.k6.io/');
}go deeper
Remember that the pool must cover iterations that overlap, not requests: multiply the rate by how long one iteration takes. At 500 per second with a 200 ms iteration you need roughly 100 VUs.
Explain where the median comes from - k6's iteration_duration on a trial run - and why sleep() inside the default function counts against the pool even though it is not doing work.
Describe the loop you actually run: a short trial with generous maxVUs, read the summary, recompute, then pin preAllocatedVUs and guard it with a dropped_iterations threshold so a regression fails the run.
Talk about who owns these numbers over time. Iteration durations drift as the script and the system change, so the interesting design is how a suite re-derives its pool sizes rather than any single value.
## The formula k6 documents k6 does not derive the pool size for you. Its own guidance for the arrival-rate executors is: ``` preAllocatedVUs = [median_iteration_duration * rate] + constant_for_variance ``` Both inputs are things you write or measure in k6 terms. `rate` (or `startRate` plus the `stages` targets, for `ramping-arrival-rate`) is what you configured, expressed per `timeUnit`. The median iteration duration is what k6 reports as the built-in `iteration_duration` metric on a trial run. ## What counts as an iteration in k6 The duration in that formula is the whole `default` function, start to finish, as k6 measures it: - every `http` call in the iteration, including redirects and connection setup; - any parsing, `check()` calls, or JSON work you do between requests; - every `sleep()` you wrote, because a sleeping VU is still occupied. A k6 VU is single-threaded and runs exactly one iteration at a time, so anything that lengthens the function directly raises the number of VUs needed to keep starts flowing. This is the concrete reason k6's docs say **do not put a `sleep()` at the end of an arrival-rate iteration**: `rate` and `timeUnit` already space the starts, and the trailing sleep buys you nothing while multiplying the pool you must preallocate. ## Where the median comes from Do not invent the number. Get it from a real k6 run and read it out of the summary: - run the scenario briefly with a deliberately generous `maxVUs`, so it does not simply collapse before producing data; - take the **median** `iteration_duration` that k6's summary reports for that trial, since the median is the figure the formula asks for; - check `dropped_iterations` on that same trial run, because a pool that ran dry distorts the durations you are about to size against; - repeat the trial after any change to the `default` function, since the pool size follows how long that function takes rather than how large the script is. ## Running the arithmetic | target | median `iteration_duration` | formula result | a reasonable `preAllocatedVUs` | |---|---|---|---| | `rate: 500, timeUnit: '1s'` | 100 ms | 50 | 65 | | `rate: 500, timeUnit: '1s'` | 200 ms | 100 | 130 | | `rate: 500, timeUnit: '1s'` | 1 s | 500 | 600 | | `rate: 500, timeUnit: '1m'` | 200 ms | ~1.7 | 5 | Two things fall out of the table. First, the `timeUnit` is part of the sum - `rate: 500` per minute is a different problem from `rate: 500` per second. Second, the pool size is dominated by how long your iteration takes, not by how many requests it makes. ## Why the first number is always provisional If you already knew the median iteration duration exactly, you would not need the run. In practice you size in a loop: 1. Take a rough guess and run the scenario briefly, with `maxVUs` set generously so the run does not simply collapse. 2. Read `iteration_duration` and `dropped_iterations` from the summary. 3. Recompute the formula with the measured median and set `preAllocatedVUs` to that plus headroom. 4. Re-run and confirm `dropped_iterations` stays at zero, then drop `maxVUs` back down to the preallocated number. Iteration duration also drifts upward during a run, so a pool sized off an early median can go short later. Headroom - the `constant_for_variance` term - is what absorbs that, and a threshold such as `dropped_iterations: ['count===0']` is what tells you it did not. ## Sizing a ramping scenario `ramping-arrival-rate` has no single `rate` to plug in. Size the pool against the **highest** `target` any stage reaches, since `preAllocatedVUs` is a single number for the whole scenario and is built before the first stage starts: ```javascript export const options = { scenarios: { ramp: { executor: 'ramping-arrival-rate', startRate: 50, timeUnit: '1s', stages: [ { target: 200, duration: '2m' }, { target: 500, duration: '5m' }, ], preAllocatedVUs: 130, }, }, }; ``` Here the peak target is 500 per second, so the same 200 ms median gives the same 130. Sizing off `startRate` instead would leave the scenario dropping starts from the moment the second stage ramps past the pool. ## The cost of guessing high Over-allocating is not free: k6 constructs every preallocated VU up front, each with its own JavaScript runtime and its own copy of per-VU state, and that memory and start-up time are paid before the first iteration. Guessing high is still the safer error, because an over-sized pool only wastes memory on the machine running k6, while an under-sized pool changes what the run actually did.
- Which stage do you size preAllocatedVUs against in a k6 ramping-arrival-rate scenario?The stage with the highest `target`. `preAllocatedVUs` is one number for the whole scenario and is built before the first stage runs, so sizing it off `startRate` guarantees drops as soon as the rate climbs past what the pool can carry.
- Why does k6's documentation discourage a sleep() at the end of an arrival-rate iteration?The executor already spaces starts using `rate` and `timeUnit`, so the sleep adds no pacing. It does extend `iteration_duration`, and since a k6 VU runs one iteration at a time, that inflates the `preAllocatedVUs` the same rate now requires.
saying these in an interview costs you the question
- Sets preAllocatedVUs equal to the rate regardless of iteration length
- Ignores timeUnit, treating rate: 500 as always per second
- Sizes a ramping scenario off startRate instead of the peak target
- Forgets that sleep() time is part of iteration_duration
- Treats one measured median as valid for the whole run