In a k6 ramping-vus run, why are iterations interrupted during ramp-down stages, and what controls it?
answer
- dropping a VU is not instant
- a window, then a hard cut
- thirty seconds unless you say otherwise
- only downward steps trigger it
basics
~20 sk6's ramping-vus executor gives every VU it drops a gracefulRampDown window, 30s by default, to finish the iteration it is already in. A VU still working when the window closes is hard stopped mid-iteration. Setting gracefulRampDown to 0s interrupts immediately.
solid answer
~40 sA ramp-down in `ramping-vus` is two events on the same VU. At the moment the stage curve says the count should fall, k6 tells the VU to stop after its current iteration; `gracefulRampDown` later — `30s` unless you set it — k6 cancels that VU's context and cuts the iteration wherever it is. So iterations get interrupted only when they outlast the window. That matters for bookkeeping: k6 emits the `iterations` and `iteration_duration` samples only for iterations that completed, and reports the rest separately in the run line as "N complete and M interrupted iterations". The fixes are to raise `gracefulRampDown` past your typical iteration length or to shorten the iteration. `gracefulRampDown: '0s'` does the opposite, cutting every in-flight iteration at each downward step.
code
javascript · 22 linesimport http from 'k6/http';
import { sleep } from 'k6';
export const options = {
scenarios: {
ramp: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
gracefulRampDown: '90s',
},
},
};
export default function () {
http.get('https://quickpizza.grafana.com/');
sleep(30);
}go deeper
Know that gracefulRampDown exists on ramping-vus and defaults to 30s: k6 lets a VU it is dropping finish the iteration it started rather than killing it on the spot.
Explain the two moments — stop taking new iterations now, hard stop after the window — and that only a downward step in the stage curve triggers either of them.
Diagnose from the run line: interrupted iterations clustered in ramp-down stages point at a window shorter than an iteration, and you should relate the fix to what your iteration actually does.
Frame the tradeoff: a generous window keeps work whole but holds VU capacity reserved and blurs when the count actually fell; a zero window gives an exact curve and throws away in-flight work.
## The option In k6 v2, `gracefulRampDown` is a `ramping-vus` option and only a `ramping-vus` option. It is a duration string, it **defaults to `30s`**, and it answers one question: when a stage's `target` is lower than the number of VUs currently running, how long may each VU that is being dropped keep working on the iteration it is already inside? None of the other five k6 executors declare it, because none of them lower a VU count mid-scenario. It is not the same thing as `gracefulStop`, the shared per-scenario option every executor accepts, which concerns the end of a scenario rather than a ramp-down within one. ## A ramp-down is two events, not one k6 builds two execution plans for a `ramping-vus` scenario: the **raw** plan, which is the literal curve your stages describe, and a **graceful** plan, which is that curve with each downward step pushed later by `gracefulRampDown`. Those two plans drive two different actions on the same VU: - At the raw moment, k6 issues a **graceful stop** to the VU. The VU finishes the iteration it is running and then starts no more. If it happened to be between iterations, it simply stops there. - At the graceful moment — `gracefulRampDown` later — k6 issues a **hard stop**. It cancels that VU's context, which cuts the iteration off wherever it happens to be, including mid-request. | moment | what k6 does to the VU being dropped | |---|---| | the stage curve says the count falls | graceful stop: finish this iteration, start no more | | any time inside the window | the VU stays reserved to the scenario and is not handed to another one | | `gracefulRampDown` has elapsed | hard stop: the VU's context is cancelled and the iteration is cut | | the option is set to `'0s'` | the two moments coincide, so every in-flight iteration is cut at once | So a VU that finishes its iteration inside the window is never interrupted at all; only the ones still working when the window expires are. In between, that VU is still **reserved to the scenario**, which is why k6 keeps its planned VU count elevated during a ramp-down: the reservation stops another scenario from claiming a VU that is still finishing work. Setting `gracefulRampDown: '0s'` collapses the two plans into one. There is no window, the graceful stop and the hard stop happen at the same instant, and every in-flight iteration at every downward step is cut. ## What an interrupted iteration costs you An iteration that is hard-stopped is not a slow iteration — for k6's bookkeeping it is a non-iteration: - k6 emits the `iterations` and `iteration_duration` samples **only** when an iteration ran to completion, so an interrupted one contributes neither. It is invisible to anything counting iterations. - k6 counts it separately instead. The run line reports both totals, in the form `running (9m30.0s), 000/200 VUs, 12480 complete and 37 interrupted iterations`. - The HTTP request that was in flight is cancelled by the context, so it never receives a response. A run whose interrupted count is non-zero and clusters at the ramp-down stages is the signature of a `gracefulRampDown` that is shorter than your iterations. ## Watching it happen `k6 run --verbose` raises the log level to debug, and each VU logs when it starts, when it enters its graceful stop, and when it is hard-stopped. On a script whose iteration is deliberately longer than the window — say a `sleep(5)` with `gracefulRampDown: '1s'` — you can watch the hard stops land one per VU as the ramp-down proceeds. ## Choosing a value 1. **Start from your iteration length, not from the ramp.** The window has to cover a whole iteration of the exported function — every request in it plus every `sleep` — because the VU is only released at an iteration boundary. If a typical iteration takes eight seconds, the `30s` default is comfortable; if it takes a minute, the default guarantees interruptions. 2. **Push it to `0s` when you want the count honoured exactly.** With no window, the number of VUs running matches the stage curve to the second, at the price of cut iterations. 3. **Remember it costs VU capacity, not just time.** During the window k6 holds the higher count reserved, so a long `gracefulRampDown` combined with a quick down-then-up profile keeps more VUs allocated than the curve alone suggests. ## In the three-stage example With `startVUs: 0` and stages `{ '2m', 200 }`, `{ '5m', 200 }`, `{ '2m', 0 }`, only the third stage ramps down, so `gracefulRampDown` bites only there. Across those last two minutes k6 issues graceful stops continuously as the target slides toward 0, and each of those VUs has its own 30-second window before a hard stop. The first two stages are unaffected: a ramp *up* and a flat hold never drop a VU, so the option never applies to them.
- Which of k6's six executors accept gracefulRampDown?Only `ramping-vus`. The other five — `constant-vus`, `shared-iterations`, `per-vu-iterations`, `constant-arrival-rate` and `ramping-arrival-rate` — never lower a running VU count as part of their own schedule, so none of them declare the key. Putting it in one of their scenario entries fails configuration.
- What does k6 record for an iteration cut short by gracefulRampDown expiring?Nothing on the iteration metrics. k6 emits the `iterations` and `iteration_duration` samples only for iterations that ran to completion, so an interrupted one appears in neither. It is counted in the run line's separate interrupted-iterations total instead, alongside the complete count.
- Why does a k6 ramping-vus scenario keep VUs allocated after its target has dropped?Because the VUs finishing iterations inside their `gracefulRampDown` window are still reserved to that scenario. k6 keeps the planned count elevated for the length of the window so another scenario cannot claim a VU that is still working, which is why allocation can exceed what the stage curve alone suggests.
saying these in an interview costs you the question
- Thinks a ramp-down kills VUs instantly at the stage boundary
- Confuses gracefulRampDown with the end-of-scenario gracefulStop
- Believes gracefulRampDown also applies when a stage ramps up
- Says an interrupted iteration still reports a partial duration
- Assumes the default is 0s, so ramp-downs never interrupt anything