In a k6 `constant-arrival-rate` scenario with `rate: 100` and `timeUnit: '1s'`, when does each iteration start?
answer
- not a burst at each boundary
- one interval, not a bucket
- reciprocal of the arrival rate
- offsets fixed from scenario start
- 100 per second means 10 ms apart
basics
~20 sk6 spaces arrival-rate starts fractionally, not in a burst: it turns rate over timeUnit into one interval, so 100 per second plans a start roughly every 10 ms at fixed offsets from the scenario's start.
solid answer
~40 sIn k6 v2 the arrival-rate executors space iteration starts **fractionally** rather than firing `rate` iterations at each period boundary. k6 reduces `rate / timeUnit` to a single arrival rate and takes its reciprocal as the inter-start interval, so `rate: 100` with `timeUnit: '1s'` plans a start about every 10 ms. Because the plan is expressed as absolute offsets from the scenario's start time — the n-th start at n × interval — a start that lands late does not push later ones back, and the schedule does not accumulate drift. It also means `timeUnit` is only the unit you write the rate in: `rate: 6000, timeUnit: '1m'` is the same 10 ms spacing.
code
javascript · 20 linesimport exec from 'k6/execution';
import http from 'k6/http';
export const options = {
scenarios: {
search_flat_100: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '30s',
preAllocatedVUs: 50,
maxVUs: 200,
},
},
};
export default function () {
console.log(exec.scenario.iterationInTest, Date.now());
http.get('https://example.com/search?q=laptop');
}go deeper
Recall that k6 spreads arrival-rate starts across the period rather than firing them together, so 100 iterations per second means roughly one start every 10 milliseconds.
Explain that k6 collapses rate over timeUnit into one arrival rate and uses its reciprocal as the inter-start interval, which is why the same rate written per second or per minute schedules identically.
Point out that starts are planned at absolute offsets from the scenario's start, so delays do not compound and a start that cannot be served is not deferred into a later slot but simply does not happen.
Treat the spacing rule as part of the contract the script declares: rate and timeUnit fix the plan k6 will follow, so reviewers can read the shape of a run out of the scenario block without rerunning it.
## The question the docs answer with one word: fractionally k6 v2's arrival-rate executors — `constant-arrival-rate` and `ramping-arrival-rate` — do **not** release `rate` iterations together at the top of each `timeUnit`. The documentation states the rule directly: *iteration starts are spaced fractionally*. k6 turns your `rate / timeUnit` pair into a single arrival rate, takes its reciprocal to get one **inter-start interval**, and plans a start at each multiple of that interval. With `rate: 100` and `timeUnit: '1s'` the interval is 1 s ÷ 100 = **10 ms**. So the plan is a start at roughly 10 ms, 20 ms, 30 ms and onward, not a hundred starts fired at 0.000 s and then 0.999 s of silence. The docs use the same rule at a smaller number: at a `rate` of `10` with a `timeUnit` of `1s`, each iteration starts about every tenth of a second. ## Why `timeUnit` cannot change the spacing on its own Because k6 reduces the pair to one rate before scheduling, `timeUnit` is a unit of *expression*, not a batching window: | configuration | arrival rate | inter-start interval | |---|---|---| | `rate: 100`, `timeUnit: '1s'` | 100/s | ~10 ms | | `rate: 6000`, `timeUnit: '1m'` | 100/s | ~10 ms | | `rate: 1`, `timeUnit: '10s'` | 0.1/s | ~10 s | | `rate: 10`, `timeUnit: '1s'` | 10/s | ~100 ms | Reading the table row by row: - The **first two rows are the same schedule written two ways** — 6000 per minute reduces to 100 per second, and k6 plans identical 10 ms gaps for both. - The **third row is what `timeUnit` is actually for**: `rate` is an integer, so a rate below one per second is expressed by widening the unit rather than by writing a fraction. - The **fourth row is the figure k6's own documentation uses** to state the rule, and it is the easiest one to hold in your head: 10 per second is one start each tenth of a second. - `timeUnit` accepts any positive duration string, sub-second values included, so `'500ms'` is a legal unit; k6 only requires it to be greater than zero. ## The schedule is absolute, not relative The second half of the mechanism matters more in practice than the first. k6 plans the n-th start at **n × interval measured from the scenario's start time**, not at *interval after the previous start actually began*. Two consequences: 1. **Delays do not compound.** If one start lands late — a busy machine, a slow VU hand-off — the following start keeps its own absolute offset, so the schedule pulls itself back rather than drifting further behind with every iteration. 2. **A late start is not a deferred slot.** k6 does not push a missed start into the next gap and run two later. If no virtual user is free at a planned instant, that start does not happen at all; k6 records it on the `dropped_iterations` counter and moves to the next planned instant. (Sizing the pool so this does not happen is a separate subject from the spacing itself.) Both follow from the same loop: each planned start's wait is computed as *its absolute offset minus the time already elapsed in the scenario*, so the plan never re-bases on what actually happened. ## What this changes about how you read a run - **Instantaneous rate is smooth by construction.** Within any window you sample, starts are evenly spread rather than clustered at period boundaries, so a per-second bucket of the run looks flat rather than saw-toothed. - **Ramping stages share the mechanism.** In `ramping-arrival-rate` the interval simply shrinks or grows as the rate walks linearly toward each stage's `target`; the spacing rule is identical, only the rate feeding it moves. - **Don't pace the iteration yourself.** k6's own guidance for both arrival-rate executors is that a trailing `sleep()` in the iteration is unnecessary, because the executor already paces starts through `rate` and `timeUnit`. ## Reading it back from a script Because starts are planned rather than batched, you can see the spacing directly. Timestamping the beginning of each iteration alongside `exec.scenario.iterationInTest` from the `k6/execution` module shows consecutive starts about 10 ms apart at `rate: 100`, and the gaps staying near 10 ms rather than growing, which is the absolute-offset behaviour visible in the data. ## The three claims this rules out - **`timeUnit` is not a bucket k6 empties.** Nothing accumulates over the period to be released at its edge; every start has its own planned instant inside the period. - **A rate written per minute is not coarser than the same rate per second.** k6 keeps one arrival rate and one interval, whichever way you chose to write numerator and denominator. - **A start that could not run is not owed to you later.** k6 goes on to the next planned instant rather than running two back to back to catch up.
- Do rate 100 per 1s and rate 6000 per 1m produce different start spacing in k6?No. k6 reduces `rate / timeUnit` to one arrival rate before it schedules anything, so both are 100 per second and both plan a start roughly every 10 ms. `timeUnit` exists so you can express rates that an integer `rate` cannot, such as `rate: 1, timeUnit: '10s'`.
- In a k6 ramping-arrival-rate scenario, how does the spacing behave inside a ramping stage?The same way, with a moving interval. k6 walks the arrival rate linearly toward the stage's `target`, so the gap between starts shrinks as the rate climbs and widens as it falls. The spacing rule does not change; only the rate feeding it does.
saying these in an interview costs you the question
- Says all rate iterations fire at each period boundary
- Treats timeUnit as a bucket k6 empties at once
- Thinks a per-minute timeUnit makes k6 check the rate once a minute
- Believes a late start pushes every following start back
- Assumes a missed start is queued and run in the next gap