skip to content

In a k6 `constant-arrival-rate` scenario with `rate: 100` and `timeUnit: '1s'`, when does each iteration start?

level: middleimportance: should knowfreq 48%

answer

  1. not a burst at each boundary
  2. one interval, not a bucket
  3. reciprocal of the arrival rate
  4. offsets fixed from scenario start
  5. 100 per second means 10 ms apart

basics

~20 s

k6 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 s

In 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 lines
javascript
import 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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