In a Gatling injection profile, when does the next chained injection step begin?
answer
- started, not finished
- steps overlap on purpose
- concurrency is not the injection rate
- use an explicit waiting step for a gap
- andThen is the barrier, chaining is not
basics
~20 sThe next step begins as soon as every virtual user the current step defines has started, not when those users have finished. A Gatling injection profile is a schedule for launching users, so consecutive steps deliberately overlap.
solid answer
~40 sWhen you pass several injection steps to one `injectOpen`, `injectClosed` or Scala `inject` call, Gatling moves to the next step the moment the current one has finished **starting** its users. It does not wait for them to complete their scenario. So `atOnceUsers(50)` followed by a sixty-second `constantUsersPerSec(10)` step begins injecting at ten users a second almost immediately, while the first fifty are still working — and the concurrency you observe is the sum of everyone still in flight, not the figure of whichever step is currently injecting. That is also why a profile that needs a genuine gap uses an explicit waiting step, and why sequencing whole scenarios needs `andThen` on the population instead, which does wait for termination.
code
java · 24 linesimport static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
import java.time.Duration;
public class OverlappingStepsSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http.baseUrl("https://example.com");
ScenarioBuilder scn = scenario("browse").exec(http("home").get("/"));
{
setUp(
scn.injectOpen(
// all 50 have started at once, so the next step begins immediately
atOnceUsers(50),
// these 10 per second overlap the 50 that are still running
constantUsersPerSec(10).during(Duration.ofMinutes(5))
)
).protocols(httpProtocol);
}
}go deeper
Be able to state the handover rule in one sentence: the next step starts once the current step has started all its users.
Be ready to work out the concurrency an overlapping profile actually produces, and to say why it appears nowhere in the profile text.
Be ready to review a colleague's profile and spot where they meant a barrier and wrote a chained step instead.
Own the convention for how your teams express phased runs, so that overlap is always deliberate rather than an artefact of the step list.
An injection profile is a list of steps, and the single most misread thing about it is the handover rule between them. ## The rule > The next injection step will start when all the virtual users defined by the current one have **started**. Started — not finished, not completed, not "returned to the pool". An injection step's job is to decide *when users are launched*; what those users then do, and how long it takes them, is the scenario's business and has no bearing on when the profile moves on. ## What that means step by step - `atOnceUsers(50)` starts all fifty in one go, so the profile hands over essentially immediately. - `rampUsers(100).during(Duration.ofMinutes(1))` has started its hundredth user at the one-minute mark, so the next step begins there — even if user one is still mid-scenario. - `constantUsersPerSec(20).during(Duration.ofSeconds(30))` hands over after thirty seconds, having launched roughly six hundred users, of which an unknown number are still running. - `nothingFor(Duration.ofSeconds(10))` exists precisely because handover is immediate otherwise: it is how you deliberately insert dead time between steps. ## The consequence people get wrong Because steps overlap, **the number of users running at any instant is not the number the current step is injecting**. A profile written as ```java scn.injectOpen( atOnceUsers(50), constantUsersPerSec(10).during(Duration.ofMinutes(5)) ) ``` does not mean "fifty users, then ten a second". It means "fifty users launched now, and from now on ten more every second for five minutes". If each user's scenario happens to take twenty seconds, you will see far more than ten users at a time — the survivors of every second that has already gone by, plus whatever is left of the original fifty. That number appears nowhere in the profile text. This is why reading the report matters: Gatling plots the users start rate and the number of concurrent users as **separate** series, and only the first of them mirrors what an open profile literally declared. ## The three timing rules, side by side | construct | scope | hands over when | |---|---|---| | chained steps in one injection call | one population's schedule | the current step has **started** all its users | | `andThen` on a population | whole populations | every user of the previous generation has **terminated** | | `maxDuration` on `setUp` | the whole run | the wall clock expires, whatever is still running | Mixing the first two up is the classic error. Someone wanting a preparation phase writes it as a first injection step, discovers the load phase started on top of it, and concludes Gatling ignored the order — when in fact the order was honoured exactly, at the granularity the construct actually operates on. ## Two constraints on the step list itself 1. **All steps in one call must be of the same model kind.** You cannot mix an open step and a closed step in one `injectOpen`, `injectClosed` or `inject` call; the compiler rejects it, either on the parameter type or on the failed implicit lookup in Scala. 2. **An empty step list is rejected.** Scala's `inject` requires a non-empty collection and fails with *Calling inject with empty injection steps* rather than quietly registering a population that never injects anything. ## How to get a real barrier If what you actually need is "finish this, then start that", the injection profile is the wrong tool no matter how you order the steps. Register two populations and chain them with `andThen`, which blocks on termination. Keep chained steps for what they are good at: describing one continuous arrival schedule whose phases are meant to overlap. ## Working out the timeline from the step list Because handover is start-based, the start time of each step is just the running total of the durations before it — and steps that start everyone at once contribute nothing to that total. Read a profile like this: 1. Set a cursor at zero. 2. For each step in order, note that it begins at the cursor. 3. Advance the cursor by that step's own duration — whatever `.during(...)` says, or `eachLevelLasting(...)` in the `incrementUsersPerSec` and `incrementConcurrentUsers` stairs helpers, or the length of an explicit waiting step. A step with no duration advances the cursor by nothing. So `atOnceUsers(20), rampUsers(100).during(60), nothingFor(30), atOnceUsers(50)` puts twenty users in at second 0, ramps a hundred more between seconds 0 and 60, does nothing until second 90, and drops fifty in at second 90. The whole profile has finished *injecting* at second 90 — and the run keeps going until the last of those users finishes its scenario, which is a different and unknown time. That final point is the practical one. **The end of the injection profile is not the end of the run.** A population is finished when its last user terminates, and that is also the moment any `andThen` children of it are unblocked. ## A note on the closed model The same rule is written once and applies to both model kinds, but its effect reads differently. Closed-model steps all carry an explicit duration, though not all of them spell it `.during(...)`: `constantConcurrentUsers(n).during(d)` and `rampConcurrentUsers(a).to(b).during(d)` do, while the stairs helper `incrementConcurrentUsers(n).times(levels).eachLevelLasting(d)` spells it `eachLevelLasting`. Either way the handover is the end of that duration, and there is no equivalent of `atOnceUsers` that hands over instantly. What does not change is the important half: reaching the end of a closed step's duration does not force the users it was maintaining to stop. They terminate only by completing their scenario, so a step that follows a closed one can still overlap the tail of the previous one's population.
- How do you insert a real gap between two Gatling injection steps?With an explicit waiting step, `nothingFor(duration)`, placed between them. It occupies the schedule for that duration without launching anyone. It is not a barrier, though: users started by the previous step keep running through the gap, so it delays the next launch rather than draining the population.
- Why does a Gatling report show far more concurrent users than the injection rate ever mentions?Because chained steps overlap: users launched by earlier steps are still running while later steps keep launching more. Gatling plots the two as separate series for exactly that reason — the users start rate series mirrors what an open profile declared, while the concurrent users series shows how many happen to be in flight.
saying these in an interview costs you the question
- Reading a step list as sequential phases that wait for each other
- Expecting the first step's users to finish before the second starts
- Using nothingFor as a barrier that drains running users
- Mixing open and closed steps in one injection call