Open vs. Closed Injection in Gatling: A Decision Guide for Load‑Testing Workloads
Open vs. Closed Injection in Gatling – a concise decision guide that maps constraints, compares profiles, explains trade‑offs, and shows a concrete simulation with validation steps.
14 Nov 2025, 03:35 UTC

Why the Injection Profile Matters
When you design a Gatling simulation, the first decision you face is whether to model traffic as an open or a closed workload. The choice determines how users are injected, how the system is stressed, and what the resulting metrics mean. If you pick the wrong profile, you may miss queueing delays, misinterpret latency, or hide a critical bottleneck.
Decision Context & Constraints
Open‑model constraints:
- Arrival rate is the key driver (e.g., public REST API, web service).
- System can handle variable concurrency; you want to see how it behaves under sustained load.
- You need to surface queueing and back‑pressure effects.
Closed‑model constraints:
- Concurrency is capped by the system under test (e.g., fixed thread pool, database connection pool).
- You care about the maximum sustained throughput the system can sustain.
- You want to avoid the coordinated omission problem where the test self‑throttles and masks real latency.
Compact Comparison Table
| Feature | Open Injection (constantUsersPerSec / rampUsersPerSec) | Closed Injection (constantConcurrentUsers / rampConcurrentUsers) |
|---|---|---|
| Primary Driver | Arrival rate (users/sec) | Concurrent users count |
| Throughput Outcome | Resulting TPS depends on response time | Resulting TPS is an outcome, not a target |
| Queueing Visibility | Explicitly exposed; users wait in queue | Implicit; users are blocked by system limits |
| Coordinated Omission | Low risk – test generates load regardless of delays | High risk – test may wait for a response before injecting next user |
| Use Case | Public APIs, services with elastic scaling | Internal services, bounded resource pools |
| Typical Assertion Mapping | Latency percentiles, response time thresholds | Throughput limits, max concurrent users reached |
| Combining with Throttle | Throttle caps the rate while open model provides the load source | Throttle can be used but offers limited benefit when concurrency is already capped |
Trade‑Offs & Practical Implications
1. Load Accuracy: Open models produce a fixed arrival stream; if the target system slows down, the number of active users rises, revealing queueing. Closed models keep the active user count fixed, so you may overlook that the system is unable to serve requests fast enough.
2. Result Interpretation: With open injection, the responseTime() percentiles are the primary metric. With closed injection, you often look at global().successfulRequests().percent() and global().requestCount() to gauge throughput.
3. CI Integration: Open models work well with latency assertions that enforce SLAs (e.g., percentile(99).lt(500)). Closed models are better suited for asserting a maximum sustained TPS (e.g., global().requestsPerSec().lt(1200)). Mixing both in a single setUp is supported, allowing you to test a public API and an internal service side‑by‑side.
4. Resource Footprint: Open injection can generate a high number of concurrent users if the response time is long, potentially exhausting client resources. Closed injection keeps the number of simulated users bounded, which can be safer on resource‑constrained test machines.
Concrete Implementation Example
Below is a minimal Gatling 3.x (Scala DSL) simulation that demonstrates both injection styles, common assertions, and a simple validation strategy. Replace http://localhost:8080/api with your test endpoint.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
class OpenVsClosedSimulation extends Simulation {
val httpConf = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json")
// Open scenario – constant arrival rate
val openScenario = scenario("Open‑Model API")
.exec(http("GET /api")
.get("/api"))
.inject(rampUsersPerSec(10).to(100).during(5.minutes),
constantUsersPerSec(100).during(10.minutes))
.assertions(
global().responseTime().percentile(99).lt(500),
global().successfulRequests().percent().gt(99.0)
)
// Closed scenario – fixed concurrency
val closedScenario = scenario("Closed‑Model Service")
.exec(http("GET /api")
.get("/api"))
.inject(rampConcurrentUsers(20).to(200).during(5.minutes),
constantConcurrentUsers(200).during(10.minutes))
.assertions(
global().requestCount().lte(2000), // example throughput limit
global().successfulRequests().percent().gt(95.0)
)
setUp(
openScenario.injectOpen,
closedScenario.injectClosed
).protocols(httpConf)
}
Explanation of key parts:
rampUsersPerSec(10).to(100).during(5.minutes)starts at 10 users per second and linearly increases to 100 over five minutes. TheconstantUsersPerSec(100).during(10.minutes)keeps the arrival rate steady.- For the closed model,
rampConcurrentUsers(20).to(200).during(5.minutes)sets a maximum of 200 concurrent users. TheconstantConcurrentUsers(200).during(10.minutes)holds that level. - Assertions are tied to the SLA you care about. Open scenario asserts latency; closed scenario asserts throughput and success rate.
Validation Strategy
- Run the simulation locally and generate the HTML report:
gatling -s OpenVsClosedSimulation. - Open
target/gatling/OpenVsClosedSimulation/xxxx/and inspect the Active Users Over Time chart. The open scenario should show a rising active‑user count during the ramp and a plateau at 100 users/sec. The closed scenario should stay flat at 200 users. - Check the Response Time Percentiles chart. If the target endpoint is slow, the open scenario’s p99 will increase, causing the assertion to fail. The closed scenario’s request count should not exceed the set limit.
- Introduce a deliberate delay in the endpoint (e.g., add
Thread.sleep(200)in the handler) and re‑run. The open scenario should now fail the latency assertion, while the closed scenario’s throughput assertion may still pass because it only measures the number of concurrent users. - In CI, add a global assertion that the open scenario’s p99 latency is below 500 ms. If the test environment is overloaded, the build will fail, giving an early warning.
Limitations & Caveats
- DSL method names differ between Gatling 3.x (Scala) and newer Java DSL releases. Verify that
constantUsersPerSecandconstantConcurrentUsersexist in the version you use. - Throttling (
throttle()) only caps the request rate; it does not generate load. Combine it with an open injection if you need to enforce a hard ceiling on TPS. - Short runs produce noisy percentile data. Use at least a 5‑minute steady‑state window for reliable p99 measurements.
- Feeder‑driven scenarios can inadvertently block the injection if the feeder is exhausted or slow. Use circular or shuffle feeders sized for the full run.
Conclusion
Choose an open injection profile when you need to model traffic arrival rates that drive the system and expose queueing. Opt for a closed profile when the system’s concurrency is bounded and you want to measure the maximum throughput it can sustain. By aligning your injection choice with the SLA and system architecture, and by validating with the HTML report and CI assertions, you can ensure that Gatling accurately reflects real‑world performance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.