Open vs. Closed Workload Models in Gatling: Picking the Injection Profile That Matches Reality
Gatling's open and closed injection models measure different things. Learn how coordinated omission can flatter your latency numbers, and how to pick the right profile for checkout-style APIs versus capacity-bound internal services.
15 Feb 2026, 01:09 UTC

The most consequential line in a Gatling simulation isn't the scenario — it's the injection profile at the bottom. Two teams can test the same endpoint with the same "load" and get wildly different p99 latencies, purely because one chose an open workload model and the other a closed one. The takeaway: decide whether your real traffic is arrival-driven or concurrency-limited before you write a single exec, because the model determines what your results actually mean.
Two mental models of load
Gatling supports two families of injection steps, and they answer different questions.
Open workload steps — rampUsersPerSec, constantUsersPerSec — inject new virtual users at a rate you control, regardless of how the system is coping. If the server slows down, users keep arriving. This models externally driven traffic: public APIs, web pages, mobile clients. Nobody on the internet waits for your server to catch its breath before clicking.
Closed workload steps — constantConcurrentUsers, rampConcurrentUsers — hold a fixed pool of concurrent users, each looping through the scenario. A new iteration only starts when the previous one finishes. This models capacity-constrained callers: an internal service with a fixed connection pool, a worker fleet of known size, a message consumer with bounded parallelism.
The trap: coordinated omission
Here's why the choice matters beyond aesthetics. In a closed model, virtual users synchronize with the system under test: when the server slows, users issue requests less often, which gives the server room to recover, which makes the next requests look fast. The test quietly stops measuring the bad moments. This is the "coordinated omission" problem, and it can make a saturated service look healthy in your report.
With an open model, arrivals continue at the configured rate even when the system is drowning. Queueing shows up honestly as rising response times and eventually timeouts. If your SLO is about what real users experience, that's the behavior you want to observe.
A worked example: checkout API
Say you're load-testing a checkout endpoint whose traffic is driven by real shoppers — an open system. A reasonable profile in Gatling's Scala DSL (syntax shown for the 3.x line; confirm exact method names against your installed version, since the DSL has shifted between releases):
setUp(
checkoutScenario.inject(
rampUsersPerSec(1).to(50).during(60), // warm-up ramp
constantUsersPerSec(50).during(300) // 5-minute steady state
).protocols(httpProtocol)
).assertions(
global().responseTime().percentile(99).lt(500),
global().successfulRequests().percent().gt(99.0)
)Two things to notice. First, the andThen-style composition lets you chain warm-up, ramp, and steady-state phases in one simulation — no external orchestration needed. Second, the assertions block turns the test into a gate: if p99 exceeds 500 ms (an illustrative threshold, not a universal one), Gatling exits non-zero and your CI pipeline fails. Run it from your build tool of choice (Maven/Gradle plugin or the standalone bundle) with normal CI permissions; the only meaningful risk is pointing it at the wrong environment, so gate the target URL behind configuration.
Now contrast with the closed alternative:
setUp(
checkoutScenario.inject(
constantConcurrentUsers(50).during(300)
).protocols(httpProtocol)
)Same "50" in the config, completely different experiment. The closed version answers "what can 50 concurrent workers sustain?" — useful if the caller is, say, a fixed pool of integration threads. It does not answer "what happens when 50 shoppers per second arrive?"
How to choose, in practice
- Traffic originates from unbounded external clients (browsers, public API consumers)? Use an open model, sized from observed arrival rates in your access logs or APM.
- Traffic originates from a bounded set of callers (thread pools, K8s job replicas, queue consumers)? Use a closed model, sized to that pool.
- Unsure? Run both against a staging endpoint and compare throughput and p99. If the closed run reports suspiciously better latency at similar throughput, coordinated omission is likely flattering your numbers.
Limitations worth respecting
The model choice fixes your semantics, but not your physics. An underpowered load generator — too few CPU cores, a saturated NIC, a shared CI runner — will distort percentiles no matter which profile you pick; watch Gatling's own injector metrics and scale out with multiple injectors if the generator is the bottleneck. Also remember that percentile thresholds in assertions are only as meaningful as the SLO behind them; treat any numbers in examples as placeholders for your own targets.
Closing
Before your next Gatling run, write one sentence describing who generates your traffic and whether they wait. That sentence picks the injection model for you. Then verify the choice empirically: run a short open-profile test and a short closed-profile test against the same staging endpoint and compare the reported p99 — the difference is the modeling error you were about to ship to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.