When a k6 arrival-rate scenario has no free VU at an iteration's scheduled start, what does k6 do?
answer
- no queue, no retry
- a counter goes up by one
- growth is asynchronous and lossy
- the warning fires only at the ceiling
- dropped_iterations, logged once
basics
~20 sAn arrival-rate start that finds no free VU is abandoned and counted on k6's built-in dropped_iterations counter. If maxVUs headroom remains k6 begins building one more VU in the background; at the ceiling it logs an Insufficient VUs warning.
solid answer
~40 sIn k6 v2 the arrival-rate executors hand each scheduled start to an idle VU. If none is idle, the start is not queued and not retried - k6 emits one sample on the built-in `dropped_iterations` counter, tagged with the scenario, and moves to the next scheduled time. It then looks at its remaining headroom: if `maxVUs` is above `preAllocatedVUs` and some of that budget is unused, it signals a background goroutine to initialize one more VU, non-blockingly, and the start that triggered it is still lost. Once the headroom is exhausted it logs, once for the whole scenario, `Insufficient VUs, reached N active VUs and cannot initialize more`, where N is the scaled `maxVUs`. The counter shows up in the end-of-test summary's execution block, and you can assert on it with a threshold.
code
javascript · 20 linesimport http from 'k6/http';
export const options = {
scenarios: {
drains_the_pool: {
executor: 'constant-arrival-rate',
rate: 200,
timeUnit: '1s',
duration: '30s',
preAllocatedVUs: 5,
},
},
thresholds: {
dropped_iterations: ['count===0'],
},
};
export default function () {
http.get('https://test.k6.io/');
}go deeper
Learn the name dropped_iterations and what it means: k6 wanted to start an iteration, had no free VU, and gave up on it. It appears in the execution section of the end-of-test summary.
Be able to walk the sequence: abandon the start, add one to the counter, optionally kick off one background VU build, and warn once when the ceiling is reached. Stress that the triggering start is lost either way.
Show that you instrument for this rather than eyeball it - a threshold on dropped_iterations turns a silently short run into a failed one, and the scenario tag tells you which scenario produced the samples.
Decide what a non-zero counter should mean for a suite: whether any drop fails the run outright or a budget is allowed, and how that choice interacts with the pool sizes you standardise on.
## The moment a start finds no free VU Each k6 VU is single-threaded and runs one iteration at a time, so an arrival-rate executor can only start as many concurrent iterations as it has idle VUs. When the scheduler's timer fires for the next start, the executor tries a non-blocking handoff to its VU pool. If that handoff fails, k6 takes four steps in order: 1. It abandons the start. There is no queue, no retry and no catch-up burst later - the source comment is explicit that the iteration is not recovered. 2. It pushes one sample of value `1` onto the built-in `dropped_iterations` metric, carrying the scenario's tags. 3. If the unused part of the `maxVUs - preAllocatedVUs` budget is still greater than zero, it sends a non-blocking signal to a background goroutine to initialize one more VU and decrements the budget. 4. If that budget has reached zero, it logs the `Insufficient VUs` warning - but only the first time. Step 3 does **not** rescue step 1. The build happens asynchronously; the start that provoked it is already counted as dropped. ## dropped_iterations, the counter - It is a built-in **Counter** named `dropped_iterations`, registered by k6 whether or not your script mentions it. - It counts *starts k6 gave up on*, never failed requests, failed `check()` calls, or iterations that threw. - It only ever increases, and there is no matching "recovered" metric. - In the end-of-test summary it is grouped with the execution metrics, alongside `vus`, `vus_max`, `iterations` and `iteration_duration`. - The arrival-rate executors are not its only source: the iteration-count executors also drop iterations when their `maxDuration` expires. Reading a number without knowing which scenario produced it is therefore ambiguous, which is why the scenario tag matters. ## The Insufficient VUs warning The exact text is `Insufficient VUs, reached N active VUs and cannot initialize more`, logged at WARN level, where `N` is the executor's `maxVUs` after execution-segment scaling. Two properties of it matter in practice: - It is emitted **at most once per executor run**, guarded by a flag inside the executor. A run that drops fifty thousand iterations logs it once. - It is emitted **only** when the growth budget is exhausted. Drops that happen while the pool is still growing produce no warning at all. | what you observe | what it says about the pool | |---|---| | no drops, no warning | the pool covered every scheduled start | | drops, no warning | the pool never reached `maxVUs`; it was still growing, or `maxVUs` was never the constraint | | drops and the warning | the pool hit `maxVUs` at least once and could not grow further | ## What the counter is not Three neighbouring numbers in a k6 run are easy to confuse with it: - **Interrupted iterations.** k6's progress line reports `X complete and Y interrupted iterations`. An interrupted iteration *did* start on a VU and was cut short, typically at the end of the scenario; a dropped one never started at all. - **Failed checks.** `check()` writes to the built-in `checks` Rate metric and returns a boolean. It never aborts an iteration and never touches this counter. - **Failed requests.** `http_req_failed` is a Rate over responses. An iteration whose every request failed still ran, and still counts as an iteration. ## Asserting on it `dropped_iterations` is a Counter, so a threshold on it is written against `count` (how many were dropped) or `rate` (dropped per second). The strictest useful form is `count===0`: ```javascript export const options = { thresholds: { dropped_iterations: ['count===0'], }, }; ``` A run in which nothing was dropped still has this threshold evaluated at the end - the counter's `count` is simply `0`, which satisfies the expression. There is no need to guard against "the metric produced no samples". ## The pool running dry, end to end ```javascript export const options = { scenarios: { drains_the_pool: { executor: 'constant-arrival-rate', rate: 200, timeUnit: '1s', duration: '30s', preAllocatedVUs: 5, }, }, }; ``` Five VUs, and a start scheduled every 5 ms. Unless an iteration finishes in about 25 ms, the five are permanently busy. Because `maxVUs` is unset it equals `preAllocatedVUs`, so the growth budget is zero from the first tick: k6 logs the warning almost immediately with `N` of 5, and every start it cannot place lands on `dropped_iterations` for the rest of the thirty seconds. The `iterations` count at the end will be far below `200 * 30`, and the gap is exactly what the counter holds.
- Does k6 replay dropped iterations later so the total iteration count still matches rate times duration?No. The executor abandons the start outright; there is no backlog and no catch-up burst. The final `iterations` count stays below the scheduled total, and `dropped_iterations` holds the difference.
- Is a non-zero dropped_iterations always caused by an undersized arrival-rate pool?No. `shared-iterations` and `per-vu-iterations` also feed the same counter when their `maxDuration` expires before the planned iterations finish. Check which scenario the samples are tagged with before concluding anything about a pool.
- Does a failed check() add to dropped_iterations?No. `check()` records on the built-in `checks` Rate metric and returns a boolean; it never aborts an iteration. `dropped_iterations` only counts starts k6 never handed to a VU.
saying these in an interview costs you the question
- Says k6 queues dropped starts and replays them later
- Thinks dropped_iterations counts failed requests or failed checks
- Expects the Insufficient VUs warning once per dropped iteration
- Believes the warning appears as soon as preAllocatedVUs are all busy
- Assumes k6 blocks the start until a new VU finishes initializing