Gatling's Open vs Closed Injection Model: The Choice That Changes What Your Test Proves
Closed-model injection hides saturation behind polite virtual users. Here is how Gatling's open and closed steps differ, and how to pick one deliberately.
20 Nov 2025, 15:14 UTC

Most Gatling load tests that pass in staging and then fall over in production were never testing the thing that broke. The usual culprit is not the scenario, the feeder, or the assertions — it is the injection model. Gatling's inject(...) chain decides whether your virtual users behave like a fixed population that waits for each response, or like an arrival stream that keeps coming whether or not the system is keeping up. Those two choices produce different results against the same target, and only one of them resembles production traffic.
What the injection model actually controls
A Gatling simulation is a setUp block containing one or more scenarios, and each scenario gets an inject(...) call. That call takes a chain of injection steps. Each step is an independent object, so a workload profile is composed from parts rather than configured in one place — which is why it is easy to add rampUsers without noticing that the profile is now closed-model.
The distinction that matters is whether a step counts concurrent users or arrivals per second.
Closed model: a bounded population that waits
atOnceUsers, rampUsers and constantConcurrentUsers are closed-model steps. They hold a bounded number of virtual users, and each user sends its next request only after the previous response has come back. Throughput is therefore a consequence of response time: if the target slows down, users slow down with it, and the request rate falls.
That self-throttling is convenient when the goal is capacity for a known user population, and it is also why a closed run can look stable while the system is saturated. The load generator is politely waiting.
Open model: arrivals that do not wait
constantUsersPerSec, rampUsersPerSec and stressPeakUsers are open-model steps. They model arrivals independent of response time — the shape of real traffic, where new requests come from clients that have no idea the backend is struggling. When the target slows under an open model, in-flight requests accumulate and queueing becomes visible instead of being hidden by the generator's own pacing.
A worked injection profile
The following Scala DSL sketch mixes a warm-up ramp with a steady-state plateau. It is illustrative and was not executed for this article — injection step names, defaults and available steps differ across Gatling versions and between the Scala and Java DSLs, so confirm each method against the API reference for the version pinned in your build.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class CheckoutSimulation extends Simulation {
val httpProtocol = http.baseUrl("https://api.example.test")
val scn = scenario("checkout")
.exec(http("browse").get("/products"))
.pause(1)
.exec(http("add-to-cart").post("/cart")
.body(StringBody("""{"sku":"ABC-1"}""")).asJson)
setUp(
scn.inject(
nothingFor(10.seconds), // gap before load starts
rampUsersPerSec(1).to(20).during(2.minutes), // open: arrivals ramp up
constantUsersPerSec(20).during(5.minutes) // open: steady arrivals
)
).protocols(httpProtocol)
.assertions(
global.failedRequests.percent.lt(1.0),
global.responseTime.percentile95.lt(800)
)
}
Two details are easy to miss. First, nothingFor is a gap in the profile, not a pause inside a scenario — it delays the start of the following step. Second, the assertions turn the run into a pass/fail gate rather than a chart you inspect afterwards; without them, "the test passed" only means the JVM did not throw.
Feeders are consumed at the arrival rate
Feeders supply per-user data, and the injection model determines how fast that data is drained. Under a closed model, a slow target slows feeder consumption too. Under an open model, arrivals keep coming at the configured rate regardless of latency, so a finite feeder can be exhausted early. Gatling's default feeder strategy behaves as a queue that fails the run when it empties; circular and random strategies wrap instead. Size the data set against arrival rate times run duration, or choose a wrapping strategy deliberately.
Trade-offs and limits
| Concern | Closed model | Open model |
|---|---|---|
| What is held fixed | Concurrent users | Arrivals per second |
| Effect of a slow target | Request rate drops | In-flight requests accumulate |
| Generator cost | Lower, self-limiting | Higher, keeps pushing |
| Best suited to | Capacity for a known population | Traffic-shaped load and overload behaviour |
The limitation to state plainly: an open-model run can be bottlenecked by the load generator itself. If the generator runs out of CPU or memory, the arrival rate you configured is not the arrival rate you achieved, and latency attributed to the target may partly be your own machine. A passing closed-model run is also not evidence of production readiness, because it cannot reproduce arrival-driven overload by construction.
How to check which model you are actually running
- Run a minimal simulation with a single injection step and inspect the generated HTML report. The active-users and requests-per-second charts show the shape the model produces — a flat active-user line indicates closed, a flat arrival line indicates open.
- Run the same target at matched nominal load once with
constantUsersPerSecand once withconstantConcurrentUsers, then compare response-time percentiles. Divergence between the two runs is the signal worth investigating. - Check the Gatling version and DSL in your build file, then confirm every injection method you use appears in that version's API reference.
- Watch load-generator CPU and memory during the run, and confirm the generator is not the limiting factor before drawing conclusions about the target.
If you change only one thing in an existing simulation, change the injection step so it matches the traffic you actually expect — then keep the closed-model run alongside it, because the two answer different questions and neither one alone tells you whether the system is ready.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.