In Gatling, rampUsers(1000).during(60) and stressPeakUsers(1000).during(60) both start 1000 users over 60 seconds — what actually differs?
answer
- Same totals, different curve
- Straight line against an S-curve
- One is even, one is middle-heavy
- rampUsers linear, stressPeakUsers Heaviside
basics
~20 sOnly the arrival shape. Both start exactly 1000 users and both occupy 60 seconds. The ramp spaces arrivals evenly; the stress peak follows a smoothed Heaviside curve, so arrivals are sparse at both ends and densest in the middle.
solid answer
~60 sNothing about the totals differs — same head-count, same window, first user at offset zero and last user strictly before the end in both. What differs is the function mapping a user's index to its start time. `rampUsers` is linear in aggregate: it spreads the users evenly, so the mean arrival rate is `n / d` and the cumulative-arrivals curve is a straight line — but not at one fixed `d / n` interval, because the window is truncated to whole seconds and the head-count is integer-sharded across them (1000 users over 60 s arrive 58–63 ms apart, not every 60 ms). `stressPeakUsers` places users along a smooth approximation of the Heaviside step function, so the arrival rate accelerates into the middle of the window and decelerates out of it. Gatling's own test pins the difference: for 100 users over 5 seconds the peak lands **67** of them between 1.5 s and 3.5 s, where an even ramp would land about 40. Neither step can make a run bigger — only `n` does that.
code
kotlin · 20 linesimport io.gatling.javaapi.core.CoreDsl.*
import io.gatling.javaapi.core.Simulation
import io.gatling.javaapi.http.HttpDsl.*
import java.time.Duration
class ShapeComparisonSimulation : Simulation() {
private val httpProtocol = http.baseUrl("https://example.org")
private val scn = scenario("browse").exec(http("home").get("/"))
init {
setUp(
scn.injectOpen(
rampUsers(1000).during(Duration.ofSeconds(60)),
stressPeakUsers(1000).during(Duration.ofSeconds(60))
).protocols(httpProtocol)
)
}
}go deeper
Be ready to say that both steps start the same number of users over the same window and that only the spacing of arrivals differs between them.
Be ready to describe the even spread of a ramp against the S-curve of a stress peak, and to point at the chart where the two look different.
Be ready to justify the shape you chose for a real run and to explain why no summary number distinguishes the two after the fact.
Be ready to argue when a smoothed surge is worth its reduced reproducibility against a stated average arrival rate that anyone can recompute.
## Same count, same window, different arrival shape `rampUsers(1000).during(60)` and `stressPeakUsers(1000).during(60)` agree on everything a summary line would show. Both are count-based open injection steps, both start exactly 1000 virtual users, both occupy exactly 60 seconds of the injection profile, and both start their first user at offset zero and their last user strictly before the 60-second mark. What differs is only the **function that maps a user's index to its start time**. | property | `rampUsers(n).during(d)` | `stressPeakUsers(n).during(d)` | |---|---|---| | total users started | `n` | `n` | | window occupied | `d` | `d` | | arrival distribution | linear — evenly spread, mean gap `d / n` | Heaviside step function, smoothed | | arrivals early in the window | same rate as anywhere else, on average | sparse | | arrivals mid-window | same rate as anywhere else, on average | dense | | cumulative-arrivals curve | a straight line | an S-curve | ## How far apart the two really are Gatling's own unit tests pin both shapes with numbers rather than adjectives. - For **`rampUsers`**, five users across one second are scheduled 200 ms apart — that is the *average* gap, `d / n`, and this case happens to hit it exactly. The scheduler shards the head-count into whole seconds first, so real gaps land on `d / n` only when the count divides cleanly: `rampUsers(1000).during(60)` emits 16 or 17 users a second, i.e. gaps of 58, 59, 62 and 63 ms rather than a flat 60 ms. - For **`stressPeakUsers`**, 100 users across five seconds put the second user at **291 ms** and land **67 of the 100** between the 1.5-second and 3.5-second marks. That middle slice is 40% of the window, so an even ramp would put roughly 40 users there. The peak concentrates about two-thirds of the arrivals into the middle two-fifths of its window. Internally `stressPeakUsers` places each user by inverting the error function, which is what produces the soft shoulders at both ends and the steep middle. The name of the internal class is still `HeavisideOpenInjection`, which is why Gatling's documentation describes the step as a smooth approximation of the Heaviside step function. ## Choosing between them - Reach for **`rampUsers`** when you want the arrival rate to be *constant on average and stated up front*: `n` users over `d` averages `n / d` arrivals per second across the window, which is easy to reason about and easy to reproduce. Individual seconds get integer shares of the head-count, so the instantaneous rate wobbles by a user a second — and when `n` is smaller than the number of seconds, some seconds get nobody at all. - Reach for **`stressPeakUsers`** when you want the arrival rate itself to accelerate and then decelerate inside one step — a surge with no hard corner at either end, expressed as a single builder rather than a hand-built chain of steps. Neither choice changes how many users the run starts, so neither can be used to make a run "bigger". If you want more load you change `n`; if you want it applied faster you shorten `d`. There is a reviewability argument too. A ramp's behaviour can be recomputed by anyone reading the line — 1000 over 60 is about a user every 60 milliseconds; the scheduler actually emits 16 or 17 a second, so the real gaps are 58, 59, 62 and 63 ms. That figure is stable across Gatling versions. A peak's behaviour cannot be recomputed from the two arguments alone; you have to know the curve. When a profile is going to be read by people who did not write it, that difference matters more than the shape usually does. ## What neither step is Both are **count-first** builders: you fix the head-count and the window, and the arrival rate is a consequence. Gatling's rate-first open steps invert that — you fix arrivals per second and the head-count is a consequence. The two families can express the same profile, but the number under your control is different, and only the count-first family guarantees the exact number of users a population will start. Neither family is a closed-model step: nothing here holds a number of users *inside* the system, and every step passed to one registering call must be of the same model kind. ## Practical notes and edge cases 1. **Both degrade the same way.** With a zero duration each collapses to `atOnceUsers(n)`; with a zero count each collapses to `nothingFor(d)`. So `stressPeakUsers(1000).during(0)` is not an error, it is a 1000-user instantaneous surge. 2. **The ramp is quantised to whole seconds.** `rampUsers` truncates its window with `toSeconds` and distributes per second, so a sub-second ramp window starts nobody. `stressPeakUsers` computes its offsets in milliseconds and does not share that limitation. 3. **The step names are identical in every SDK.** Both steps are written the same way in Java, Kotlin, JavaScript, TypeScript and Scala; what differs is the registering call — `injectOpen` on the Java-API SDKs against a plain `inject` in Scala — and the duration, `Duration.ofSeconds(60)` in Java and Kotlin against `60` or `{ amount: 60, unit: "seconds" }` in JavaScript and TypeScript. 4. **A ramp is not a rate declaration.** `rampUsers(1000).during(60)` fixes the head-count and derives the rate; the rate-first builders in Gatling's open model fix the rate and derive the head-count. They can produce the same profile, but the number you control is different. ## Telling them apart after the run This is the trap worth remembering: **no aggregate distinguishes them**. Total users started, the length of the injection window, the run's duration and every summary statistic derived from them are identical whichever builder you used, so a substitution made by accident leaves no trace in the numbers a report leads with. The difference only exists in arrivals over time — flat on average at `n / d` per second for the ramp, a hump peaking well above `n / d` for the peak. The cheapest safeguard is that the injection step in the simulation source is the authoritative record of what you asked for, so keep the profile short enough to read at a glance rather than assembled at run time.
- What does each of the two steps do when the duration passed to .during is zero?Both collapse to `atOnceUsers(n)`. Gatling folds the zero-duration case rather than rejecting it, so `stressPeakUsers(1000).during(0)` is a legal way to write a 1000-user instantaneous surge. Symmetrically, a zero head-count on either step folds into `nothingFor(d)`.
- After a run, how would you tell which of the two shapes was actually declared?Not from the totals — head-count, window and every summary statistic are identical by construction. You have to look at arrivals over time, where a ramp is flat on average at n divided by d per second and a peak is a hump. In practice, read the injection step in the simulation source; it is the only unambiguous record.
saying these in an interview costs you the question
- Believing stressPeakUsers starts more users than an equivalent rampUsers step
- Thinking rampUsers front-loads or back-loads its arrivals
- Assuming a stress peak stretches beyond the window it was given
- Expecting the summary statistics to reveal which shape ran