skip to content

Closed Model Steps

The two closed steps that pin how many users are inside the system at once, and the termination rule that makes a declared concurrency curve a target Gatling aims at rather than enforces.

on this pageshow

explore

questions

5

In Gatling's closed injection model, how do you declare 300 concurrent users held for ten minutes and then brought down to 50?

level: juniorimportance: must knowfreq 66%

answer

  1. Closed model counts users, not rates
  2. Two primitives — hold and ramp — plus a stairs helper
  3. constantConcurrentUsers holds, rampConcurrentUsers moves
  4. injectClosed on the Java API, inject in Scala
  5. Add a trailing hold to stay at 50

basics

~20 s

Chain closed injection steps in one call: constantConcurrentUsers(300) for ten minutes, then rampConcurrentUsers(300).to(50) over a wind-down window, then constantConcurrentUsers(50) if that level must be held. Java, Kotlin, JavaScript and TypeScript register them with injectClosed; Scala uses inject.

solid answer

~40 s

Gatling's closed model has two primitive level builders: `constantConcurrentUsers(n).during(d)` holds the number of users running at once, and `rampConcurrentUsers(a).to(b).during(d)` moves that number linearly between two values. A third closed entry point, `incrementConcurrentUsers(n).times(k).eachLevelLasting(d)`, is a stairs helper that Gatling expands into a sequence of those two. So the profile is `constantConcurrentUsers(300).during(Duration.ofMinutes(10))`, then `rampConcurrentUsers(300).to(50).during(Duration.ofMinutes(2))`, then a third `constantConcurrentUsers(50).during(...)` step if 50 must actually be held — the ramp only ends the profile at 50, it does not park there. Hand the chain to `scn.injectClosed(...)` in Java, Kotlin, JavaScript and TypeScript, or `scn.inject(...)` in Scala, inside the one `setUp` call in the simulation constructor. Every step in a single call must be the same model kind, so a closed step cannot sit beside an open one such as `rampUsers`.

code

java · 23 lines
java
import 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 HeldLevelsSimulation extends Simulation {

  HttpProtocolBuilder httpProtocol = http.baseUrl("https://example.com");

  ScenarioBuilder scn = scenario("browse").exec(http("home").get("/"));

  {
    setUp(
      scn.injectClosed(
        constantConcurrentUsers(300).during(Duration.ofMinutes(10)),
        rampConcurrentUsers(300).to(50).during(Duration.ofMinutes(2)),
        constantConcurrentUsers(50).during(Duration.ofMinutes(5))
      )
    ).protocols(httpProtocol);
  }
}

go deeper

for a junior

Be ready to write the two closed primitives from memory, name incrementConcurrentUsers as the stairs helper built out of them, and say which method registers a closed profile in the language you work in.

for a middle

Be ready to explain why the step names are identical across SDKs while the entry point is not, and why one call cannot mix open and closed steps.

for a senior

Be ready to review a profile and spot the missing trailing hold, a ramp window shorter than one scenario iteration, and a bare-number duration someone read as minutes.

for a principal

Be ready to set a house shape for injection profiles — which levels a suite declares, how wind-downs are expressed, and how run length is bounded across a pipeline.

## The closed model's level builders Gatling declares load as a chain of **injection steps**. The steps that talk about *concurrent users* — how many virtual users are running at the same instant — belong to the **closed workload model**. At 3.15.x there are two primitives and one helper built out of them: - `constantConcurrentUsers(n).during(d)` — hold the number of users in the system at `n` for `d`. - `rampConcurrentUsers(a).to(b).during(d)` — move that number linearly from `a` to `b` over `d`. - `incrementConcurrentUsers(n).times(k).eachLevelLasting(d)` — a **stairs** helper: `k` levels, each `n` concurrent users higher than the last. `.startingFrom(n)` and `.separatedByRampsLasting(d)` are both optional. All three return a `ClosedInjectionStep`, and all three are exported by the Scala `ClosedInjectionSupport` trait and by the Java API's `CoreDsl`. The first two are the primitives — Gatling's reference calls them the "building blocks" and gives the stairs helper a section of its own, because `incrementConcurrentUsers` is compiled down into a list of constant and ramp steps rather than being a third kind of step at run time. So "there are only two" is a fair description of the machinery and a wrong description of the API: if you are naming what you can write, name three. None of them is a rate: the argument is a **population size**, not an arrival figure, and Gatling's job is to keep that many scenarios in flight. ## Writing "300 for ten minutes, then down to 50" The worked profile is three steps in one call: 1. `constantConcurrentUsers(300).during(Duration.ofMinutes(10))` — the hold. 2. `rampConcurrentUsers(300).to(50).during(Duration.ofMinutes(2))` — the wind-down. 3. `constantConcurrentUsers(50).during(Duration.ofMinutes(5))` — only if 50 must actually be held. Step 3 is the one people forget. A `rampConcurrentUsers(300).to(50)` step ends the *profile* at 50; it does not park the run there. Once the last step's window expires Gatling stops starting users altogether, so without a trailing hold the run simply drains. ## Registering the profile: one call, one model kind The steps are handed to the scenario, and the method name depends on the authoring language: | language | entry point | |---|---| | Java, Kotlin | `scn.injectClosed(...)` | | JavaScript, TypeScript | `scn.injectClosed(...)` | | Scala | `scn.inject(...)` | The step *names* are identical everywhere — `constantConcurrentUsers`, `rampConcurrentUsers` and `incrementConcurrentUsers` are spelled the same in all five languages. Only the entry point differs, because Java, Kotlin, JavaScript and TypeScript all sit on Gatling's Java API, which exposes `injectOpen` and `injectClosed` as separate methods, while Scala has a single `inject` whose model is chosen by an implicit injection-profile factory. Saying "call `injectClosed`" without naming the language is false in Scala; saying "call `inject`" is false in the other four. The result goes into **one `setUp` call in the simulation constructor** — in Java that is the instance initializer block the recorder itself emits. Every step in a single `inject` / `injectClosed` call **must be of the same model kind**. You cannot put `constantConcurrentUsers(300)` beside `rampUsers(500)`: the Java API's `injectClosed` only accepts `ClosedInjectionStep` arguments, and in Scala no implicit factory covers a mixed sequence, so both fail to compile rather than misbehaving at run time. ## How the duration argument is spelled The step names do not change across SDKs, but the duration you hand them does: | SDK | duration argument | |---|---| | Java, Kotlin | a `java.time.Duration`, e.g. `Duration.ofMinutes(10)` | | Scala | a Scala duration literal, e.g. `10.minutes` | | JavaScript, TypeScript | an object literal, e.g. `{ amount: 10, unit: "minutes" }` | | all of them | a bare number, which means **seconds** | `constantConcurrentUsers(300).during(600)` and `constantConcurrentUsers(300).during(Duration.ofMinutes(10))` are the same profile. A snippet that shows `Duration.ofMinutes` as "the JavaScript spelling" is simply wrong. ## What the numbers do and do not promise Two things about the arguments are worth knowing before you read a report: - **The count is an integer target, re-evaluated in whole seconds.** A ramp computes `from + slope * elapsedSeconds`, truncated to a whole user. `rampConcurrentUsers(1).to(100)` over 99 minutes therefore sits at 1 for the first full minute and only then moves to 2. Over a short ramp between two close numbers the target line is a visible staircase, not a smooth slope. - **A held count fixes how many users run at once, never how many run in total.** Gatling does not preallocate a pool. It starts users on demand and starts a fresh one each time an existing one finishes, so the total is whatever the run happens to produce. ## Common mistakes - Reading `constantConcurrentUsers(300)` as "300 users per second". It is a level, not a rate; the per-second builders belong to the open model and cannot appear in the same call. - Expecting `rampConcurrentUsers(300).to(50)` to hold 50 afterwards. Add the trailing `constantConcurrentUsers(50).during(...)` step if you want that level. - Writing `injectClosed` in a Scala simulation, where the method does not exist, or `inject` in a Java one, where it is not the public entry point. - Assuming the declared window is the run's wall-clock length. It is the length of the *profile*; the run outlives it by however long the last virtual users need to finish their scenario.

  • What happens if you pass constantConcurrentUsers(300) and rampUsers(500) to the same Gatling inject call?
    It does not compile. Gatling requires every step in one call to be of the same model kind: the Java API's `injectClosed` accepts only `ClosedInjectionStep` arguments, and in Scala no implicit injection-profile factory covers a mixed sequence. Declare the open population separately if you really want both.
  • Does rampConcurrentUsers(300).to(50) move the target smoothly, or in whole users?
    In whole users, re-evaluated each second: the target is the starting value plus the slope times the elapsed whole seconds, truncated to an integer. Over a long, shallow ramp the target line is a visible staircase — `rampConcurrentUsers(1).to(100)` across 99 minutes sits at 1 for a full minute before moving to 2.
  • Can the same closed step be written with a plain number instead of a Duration?
    Yes. Every SDK accepts a bare number on `during`, and it means seconds, so `constantConcurrentUsers(300).during(600)` is the ten-minute hold. The typed forms differ by language: `java.time.Duration` in Java and Kotlin, a Scala duration literal in Scala, and an object literal such as `{ amount: 10, unit: "minutes" }` in JavaScript and TypeScript.

saying these in an interview costs you the question

  • Reading constantConcurrentUsers(300) as an arrival rate of 300 per second.
  • Mixing closed and open injection steps inside one inject call.
  • Expecting a ramp step ending at 50 to then hold 50.
  • Calling injectClosed in a Scala simulation, which only has inject.
open as a page

In a Gatling closed injection profile, what actually brings the number of concurrent users down when a step ramps from 300 to 50?

level: middleimportance: must knowfreq 54%

basics

~20 s

Only virtual users finishing their scenario. Gatling never interrupts a running user to meet a lower target: it withholds the replacement for a finished user while the population is still above the target for the current second, so concurrency falls at whichever is slower — the rate scenarios end, or the declared slope.

open as a page

While a Gatling closed injection step holds 300 concurrent users, one virtual user finishes its scenario — what does Gatling start in its place?

level: middleimportance: should knowfreq 41%

basics

~20 s

A brand-new virtual user with a fresh id and an empty session, not the finished one looping again. Gatling preallocates no user pool, so a held count fixes how many run at once, never how many run in total.

open as a page

A Gatling Java simulation injects only constantConcurrentUsers(300).during(Duration.ofMinutes(10)) — when does that run actually stop?

level: seniorimportance: should knowfreq 37%

basics

~20 s

Not at ten minutes. When the profile's window expires Gatling stops starting replacements, but the run ends only once every user it already started has finished its scenario, so the tail is roughly one scenario iteration long.

open as a page

Your Gatling run must take a service from 300 concurrent users down to 50 at the end — would you express that as a ramp step, a step change, or by ending the population, and how would you decide?

level: principalimportance: nice to knowfreq 25%

basics

~30 s

None of the three interrupts a running user, but they do not come down at the same speed. A ramp keeps replacing finished users against its falling target, so the population follows the declared slope; a step change and the end of the population both shed load at the full completion rate. Ramp when the descent itself is measured or must be gentle; step down when only the lower level matters; end the population when neither is read.

open as a page