skip to content

In a Gatling simulation, what does appending .randomized() to constantUsersPerSec(100).during(Duration.ofMinutes(10)) change about the run, and how is it spelled in Scala?

level: middleimportance: should knowfreq 40%

answer

  1. Even spacing becomes irregular spacing
  2. Only on the two rate steps
  3. Comes last, after during
  4. Scala drops the parentheses
  5. Seed comes from the clock, unpinnable

basics

~20 s

It replaces the evenly spread arrival schedule with randomized intervals drawn from a Poisson process at the same average rate. Scala writes it without parentheses, as .randomized; Java, Kotlin and the JavaScript SDK write .randomized().

solid answer

~40 s

Without it, Gatling spreads arrivals evenly across the window, so the gaps between user starts are regular. `.randomized()` swaps that for a Poisson process at the same average rate: users still arrive at about 100 a second over the ten minutes, but the intervals are irregular, so arrivals clump and thin the way independent events do. It is available only on the two rate steps — `constantUsersPerSec(...).during(...)` and `rampUsersPerSec(...).to(...).during(...)` — and it must come **last**, after `.during(...)`, because it returns a plain injection step with nothing further to chain. Scala spells it `.randomized` with no parentheses, since it is a parameterless Scala `def`; the Java API and the SDKs built on it write `.randomized()`. The seed comes from the clock, and the DSL offers no way to pin it.

code

scala · 17 lines
scala
import io.gatling.core.Predef._
import io.gatling.http.Predef._

import scala.concurrent.duration._

class RandomizedSimulation extends Simulation {

  private val httpProtocol = http.baseUrl("https://example.com")

  private val scn = scenario("randomized hold")
    .exec(http("home").get("/"))

  setUp(
    // no parentheses: `randomized` is a parameterless Scala def
    scn.inject(constantUsersPerSec(100).during(10.minutes).randomized)
  ).protocols(httpProtocol)
}

go deeper

for a junior

Be ready to recall that the modifier makes the gaps between user starts irregular while leaving the average rate and the window alone.

for a middle

Be ready to explain that it swaps an even schedule for a Poisson process, that only the two rate steps accept it, and that Scala omits the parentheses.

for a senior

Be ready to point out that the seed is unpinnable, and to say which phases of a profile you would therefore leave on the deterministic schedule.

for a principal

Be ready to argue whether a suite should carry irregular arrivals at all, given that it buys realism at the cost of a schedule you can never replay.

## What the modifier actually swaps out A plain `constantUsersPerSec(100).during(Duration.ofMinutes(10))` produces a **deterministic** arrival schedule. Gatling works out the head-count, spreads it evenly over the window, and starts users at regular gaps. Run the same simulation twice and the arrival offsets are identical. Appending `.randomized()` keeps the window and the average rate but throws away the even spacing. Internally the step is replaced by a Poisson arrival process — the standard model for independent events arriving at a given average rate. Gatling's own reference describes the difference in exactly these terms: without the modifier *"users will be injected at regular intervals"*, with it *"users will be injected at randomized intervals"*. The practical effect is **clumping**. Over ten minutes at 100 a second you still get roughly 60,000 arrivals, but some seconds carry noticeably more than 100 and others noticeably fewer, and the gap between two consecutive starts is sometimes far shorter or longer than the 10 ms an even schedule would use. ## Where it can and cannot be used `.randomized()` is not available on every injection step. It exists only on the two steps that declare an arrival **rate**: * `constantUsersPerSec(rate).during(d).randomized()` — a flat rate with irregular spacing * `rampUsersPerSec(r1).to(r2).during(d).randomized()` — a rising or falling rate with irregular spacing It is **not** available on the count-based open steps such as `atOnceUsers(n)`, `rampUsers(n).during(d)` or `stressPeakUsers(n).during(d)`, and not on the closed-model steps. The reason is visible in the type signatures: `.during(...)` on the two rate builders returns a specialised injection step that carries the modifier, while `.during(...)` on the count-based builders returns the plain step type, which has no such method. Calling it there is a compile error, not a runtime surprise. Order matters for the same reason. `.randomized()` returns a plain injection step, so nothing can follow it. You write `.during(...).randomized()`, never `.randomized().during(...)`. ## Spelling it in each SDK | SDK | how it is written | |---|---| | Java | `constantUsersPerSec(20).during(15).randomized()` | | Kotlin | `constantUsersPerSec(20.0).during(15).randomized()` | | JavaScript / TypeScript | `constantUsersPerSec(20).during(15).randomized()` | | Scala | `constantUsersPerSec(20).during(15).randomized` | Scala is the outlier, and it is a language fact rather than a Gatling one: `randomized` is declared as a parameterless `def`, which Scala convention calls without parentheses. Kotlin's `20.0` in the table is the other language-level difference — the rate parameter is a `double` and Kotlin will not widen an `Int` for you. ## What it does *not* give you Three things are worth stating explicitly. 1. **It is not reproducible.** The randomization seed is taken from the system clock when the step is built, and the public DSL exposes no parameter, overload or configuration key to set it. Two runs of the same simulation therefore get different arrival instants. If you need the same schedule twice, do not use the modifier. 2. **It does not change the average rate.** The window and the declared rate are unchanged; only the spacing within the window moves. 3. **It does not change the head-count by more than one user.** On Gatling 3.15.1 and later a randomized step starts a *fixed* number of users rather than a seed-dependent one: the mean rate times the duration in whole seconds. The plain step computes the same product but resolves it differently: it **rounds**, where the randomized replacement **truncates**. The two therefore agree on every whole product — every whole rate, including the 60,000 of the example above — and also wherever the leftover fraction is under a half, since both then land on the same whole number. They part company only when that fraction reaches a half: `constantUsersPerSec(0.4978).during(100)` works out at 49.78 users, which the plain step rounds to 50 and the randomized one truncates to 49. The gap is never more than one user, and only the flat step can show it — the ramp form truncates with or without the modifier. But "exactly the same head-count" is not what the code says. This was not always true: before 3.15.1 the modifier used a thinning algorithm whose total varied from run to run with the seed, and the Scala source comment saying so is still present and now out of date. ## When to reach for it Use it when the even, machine-regular spacing of the default schedule is itself the thing you distrust — when you want arrivals that cluster and thin the way independent requests do, and you are willing to give up a repeatable arrival schedule to get it. Keep the plain form for the phases of a profile whose numbers you intend to line up against another run, because those runs can then differ only in the system under test, never in when users started. That is a declaration-level trade-off, and Gatling gives you no dial between the two: there is no partial jitter, no percentage, no seed. You get the even schedule or the Poisson one. ## Reading it in a report afterwards One practical consequence of the clumping: at low declared rates a randomized phase looks noisier than the number you wrote. A phase declared at 5 arrivals a second will show seconds carrying 2 and seconds carrying 9, because that is what independent arrivals at an average of 5 look like. None of that is a defect in Gatling or in the system under test, and none of it is worth investigating — it is the distribution you asked for. The same wobble seen on a *plain* rate step would be worth a second look, since that schedule is supposed to be even.

  • Why can you not append .randomized() to rampUsers(500).during(60)?
    Because `rampUsers(...).during(...)` returns the plain open injection step type, which does not declare the method. Only the two rate builders return the specialised step that carries it, so the mistake fails at compile time.
  • Two runs of the same randomized profile give different arrival timings. How do you pin the seed?
    You cannot from the DSL. The seed is read from the system clock when the step is constructed, and no public overload, builder method or documented configuration key exposes it. If you need a repeatable schedule, drop the modifier.
  • Does randomizing the intervals change how many users the step starts?
    By at most one user. On Gatling 3.15.1 or later the head-count is fixed rather than seed-dependent — the mean rate times the window in whole seconds, truncated — while the plain step rounds that same product, so `constantUsersPerSec(0.4978).during(100)` starts 50 plain and 49 randomized. Otherwise only the instants move.

A metronome and rain on a roof can both average a hundred beats a second. The plain step is the metronome; .randomized() is the rain.

saying these in an interview costs you the question

  • Thinking .randomized() also randomizes the average arrival rate
  • Writing .randomized() before .during() in the chain
  • Expecting a seed parameter so the schedule can be replayed
  • Trying to append it to rampUsers or atOnceUsers
  • Writing .randomized() with parentheses in a Scala simulation