skip to content

In a Gatling simulation, which injection steps declare an arrival rate that climbs from 10 to 100 users per second over five minutes and then holds at 100, and how many virtual users does each part start?

level: middleimportance: should knowfreq 55%

answer

  1. Two steps, one registration call
  2. Ramp first, flat hold second
  3. Hold at the ramp's end rate
  4. Ramp count uses the mean rate
  5. Mean of 10 and 100 is 55

basics

~10 s

Two steps in one injectOpen call: rampUsersPerSec(10).to(100).during(Duration.ofMinutes(5)) then constantUsersPerSec(100).during(...). The ramp averages 55 arrivals a second over 300 seconds and starts 16,500 users; the hold starts 100 for every second it runs.

solid answer

~40 s

You list them as two arguments to one registration call: `rampUsersPerSec(10).to(100).during(Duration.ofMinutes(5))` followed by `constantUsersPerSec(100).during(Duration.ofMinutes(10))`. Both are open-model rate steps, so they may share a single `injectOpen(...)` — or `inject(...)` in Scala — and they run in the order written. The ramp's head-count is the *mean* of its two rates times the window: 55 a second over 300 seconds, so **16,500 users**. The hold's is simply its rate times its window: 100 a second over 600 seconds, so **60,000 users**. The hold should be declared at the rate the ramp finished on; any other value puts a step discontinuity at the join, which is almost never what a ramp-then-hold profile is trying to say.

code

java · 26 lines
java
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

import java.time.Duration;

public class RampThenHoldSimulation extends Simulation {

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

  private final ScenarioBuilder scn = scenario("ramp then hold")
    .exec(http("home").get("/"));

  {
    setUp(
      scn.injectOpen(
        // 10 -> 100 arrivals/s over 5 minutes: mean 55/s * 300 s = 16,500 users
        rampUsersPerSec(10).to(100).during(Duration.ofMinutes(5)),
        // then flat at the rate the ramp ended on: 100/s * 600 s = 60,000 users
        constantUsersPerSec(100).during(Duration.ofMinutes(10))
      )
    ).protocols(httpProtocol);
  }
}

go deeper

for a junior

Be ready to write the pair — a rate ramp followed by a flat rate step — as two arguments to a single injection call, in that order.

for a middle

Be ready to compute both head-counts on the spot, averaging the ramp's two rates, and to explain why the hold should match the ramp's target rate.

for a senior

Be ready to review such a profile for the silent defects: a mismatched join, a bare duration read as minutes, a ramp sized off one endpoint.

for a principal

Be ready to say how long each phase should be and why the profile needs a ramp at all, rather than defending a shape someone copied from a sample.

## The shape, and the two steps that spell it A ramp-then-hold profile is the most common thing the rate steps are used for: bring the arrival rate up a line, then keep it flat. Gatling has no single builder for it. You write it as **two steps in one registration call**, and the second one begins where the first one's window ends. ```java setUp( scn.injectOpen( rampUsersPerSec(10).to(100).during(Duration.ofMinutes(5)), constantUsersPerSec(100).during(Duration.ofMinutes(10)) ) ).protocols(httpProtocol); ``` Every step in a single call must belong to the same model kind, and both of these are open-model rate steps, so the pairing is legal. The order in the argument list is the order they run in. ## Counting the users each part starts The two steps use different arithmetic, and this is where reviews go wrong. | part | declaration | rate used | window | users started | |---|---|---|---|---| | the ramp | `rampUsersPerSec(10).to(100).during(Duration.ofMinutes(5))` | mean of 10 and 100 = 55/s | 300 s | **16,500** | | the hold | `constantUsersPerSec(100).during(Duration.ofMinutes(10))` | 100/s | 600 s | **60,000** | | whole profile | — | — | 900 s | **76,500** | The ramp is the one people get wrong. Its head-count is the area under a straight line, so it uses the **average** of the start and end rates — not the target rate, which would give 30,000, and not the start rate, which would give 3,000. Gatling's own unit tests pin this down: a ramp from 2 to 4 arrivals a second over 10 seconds is asserted to start 30 users, which is the mean rate of 3 times 10 seconds. One more useful consequence of that formula: a ramp is not half done when its window is half over. Halfway through the five minutes the instantaneous rate has reached 55 a second, but only about 4,875 of the 16,500 users have started — roughly 30%, because the arrivals accumulate on a curve while the rate moves on a line. ## Matching the rates at the join The hold should normally be declared at the rate the ramp ended on. Writing `rampUsersPerSec(10).to(100)` followed by `constantUsersPerSec(80)` is perfectly legal and compiles cleanly, but it puts a downward step in the arrival rate at the join — a profile shape you did not intend and that nothing in the tooling will flag. Read the two numbers together when reviewing. There is a degenerate case worth knowing. `rampUsersPerSec(100).to(100).during(d)` has zero acceleration, and it produces the same regular schedule as `constantUsersPerSec(100).during(d)` — Gatling's tests assert that a same-rate ramp yields a single repeated interval. So the two spellings coincide when the endpoints match; prefer the flat step, because it says what it means. ## Spelling it across the SDKs The step names are identical everywhere. What changes is the registration method and the duration: 1. **Java and Kotlin** — `scn.injectOpen(...)`, durations as `java.time.Duration`: `Duration.ofMinutes(5)`. 2. **Scala** — `scn.inject(...)`, durations as Scala literals: `5.minutes`. 3. **JavaScript and TypeScript** — `scn.injectOpen(...)`, durations as a bare number of seconds (`during(300)`) or an object literal `{ amount: 5, unit: "minutes" }`. 4. **Kotlin only** — the rates must be doubles: `rampUsersPerSec(10.0).to(100.0)`, because Kotlin does not widen an `Int` to the `double` parameter. Watch the bare-number form. `during(5)` is **five seconds**, not five minutes, in every SDK. A ramp-then-hold that was supposed to run fifteen minutes and instead ran fifteen seconds is a real and easily missed defect, because the run completes, the report generates and nothing errors. ## Reviewing one of these profiles Three checks catch nearly everything: * **Average the ramp's rates before multiplying.** If someone has written the expected user count in a comment, recompute it. * **Check the join.** The hold's rate should equal the ramp's `to(...)` value unless a step change is deliberate. * **Check the duration units.** A bare number is seconds; a five-minute ramp needs `Duration.ofMinutes(5)`, `5.minutes` or `during(300)`. ## Sizing the phases against each other A last point that reviews miss: the ramp is rarely the small part of the profile. In the example above the ramp starts 16,500 users before the hold begins — more than a fifth of the whole run's users — so it is not a negligible preamble that can be ignored when someone asks how many requests the run made. If you want the ramp to contribute less, shorten it or start it higher; both change its mean rate, and the head-count follows immediately from that mean. Doubling the ramp's duration doubles its users; halving the gap between its two rates does not, because what matters is the average of the endpoints, not the distance between them.

  • What happens if the hold declares a lower rate than the ramp's target?
    Nothing fails. The profile simply steps the arrival rate down at the join — from 100 a second to whatever the hold declares — and Gatling accepts it. It is a silent shape defect, so the two numbers have to be read together in review.
  • How far through the 16,500 users is the ramp when half its window has elapsed?
    About 4,875, or roughly 30%. The instantaneous rate is halfway, at 55 a second, but arrivals accumulate on a curve because the rate is still climbing, so the second half of the window starts far more users than the first.
  • Could the hold be written as a ramp instead?
    Yes. `rampUsersPerSec(100).to(100).during(d)` has zero acceleration and produces the same evenly spaced schedule as `constantUsersPerSec(100).during(d)`. Prefer the flat step: it states the intent directly and cannot be misread as a climb.

saying these in an interview costs you the question

  • Sizing the ramp from its target rate instead of the mean
  • Declaring the hold at a rate the ramp never reaches
  • Assuming half the ramp's window has started half its users
  • Writing during(5) and expecting five minutes
  • Putting an open and a closed step in the same call