Controlling Concurrency in Gatling with the Closed Model
Learn how Gatling's Closed Model maintains fixed concurrent virtual users for predictable load testing, and when to use it instead of the Open Model.
02 Oct 2026, 04:06 UTC

The Problem: Unpredictable Load Spikes
When testing a system with a strict concurrency limit—such as a database connection pool or a licensed software seat limit—using a standard "Open Model" (where users arrive at a fixed rate regardless of system response) can create unrealistic load. If the server slows down, an Open Model continues to inject new users, leading to a massive backlog of pending requests that can crash the test client or create an artificial "death spiral" that doesn't reflect real-world usage.
The Closed Model solves this by maintaining a fixed number of concurrent Virtual Users (VUs). In this model, a user only starts a new iteration after they have completed the previous one. This ensures the system is never hit with more concurrent requests than the defined user population, making it the correct choice for simulating a fixed pool of workers or clients.
How the Closed Model Works
Gatling leverages an asynchronous, non-blocking architecture powered by Akka and Netty. Unlike traditional thread-per-user tools, Gatling does not dedicate a JVM thread to every VU. Instead, it uses a small number of threads to manage thousands of state machines. When a VU in a Closed Model sends a request, it enters a waiting state; only upon receiving the response does it trigger the next step in the scenario.
Gatling's injection API has evolved across major versions. The injectClosed profile described here reflects the DSL available in Gatling 3.x. If you are upgrading from an older 2.x codebase, confirm that your population builders and duration helpers still resolve, because some injection step names and implicit conversions changed between major releases.
Implementation Example
To implement a Closed Model, you use the injectClosed method within your population profile. This requires a ClosedInjectionStep, which defines how many users are active over a specific duration.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class ConcurrencyTest extends Simulation {
val httpProtocol = http.baseUrl("http://api.example.com")
val scn = scenario("Fixed User Load")
.exec(http("Request").get("/endpoint"))
.pause(1)
setUp(
scn.injectClosed(
// Maintain 50 concurrent users for 5 minutes
closed(50).during(5.minutes)
)
).protocols(httpProtocol)
}Execution and Permissions
Run this simulation from your terminal using the Gatling bundle or Maven/SBT plugin. Ensure the machine running Gatling has sufficient ephemeral ports available, as high concurrency can lead to Address already in use errors if the OS cannot recycle ports fast enough.
- Command:
./bin/gatling.sh(ormvn gatling:test) - Permissions: Standard user permissions are sufficient, provided the firewall allows outbound traffic to the target API.
- Expected Check: In the final Gatling report, the "Active Users" graph should remain a flat line at 50 for the duration of the test.
Closed vs. Open Model: A Comparison
The primary difference lies in how the tool handles server latency. If the server response time increases from 100ms to 1s, the behavior diverges sharply:
| Metric | Open Model (injectOpen) | Closed Model (injectClosed) |
|---|---|---|
| Arrival Rate | Constant (e.g., 10 users/sec) | Variable (depends on response time) |
| Concurrent Users | Increases as response time slows | Stays constant |
| Throughput (RPS) | Attempts to stay constant | Drops as response time slows |
| Use Case | Public websites, APIs | Internal tools, Fixed-worker pools |
Limitations and Common Pitfalls
The Risk of Masking Degradation
Because the Closed Model limits the number of requests based on the server's ability to process them, it can inadvertently mask performance degradation. If your server slows down, the throughput (Requests Per Second) will drop, but the system may not crash because the load is naturally throttled. To detect this, you must monitor Response Time and Throughput simultaneously; a drop in RPS while active users remain constant is a clear indicator of a bottleneck.
Client-Side Resource Exhaustion
While Gatling's non-blocking model is efficient, it is not infinite. Each virtual user consumes memory for its scenario state, session variables, and pending response buffers. Before scaling to thousands of users, run a short smoke test at 10% of your target population and observe the load generator's CPU and heap. If the Gatling process saturates a core before the target system does, add more load generator instances rather than increasing the population on a single node.
Verification
During the run, watch the console logger for injection messages. Gatling should report the scenario starting and the closed injection step ramping to the target population. After the test, open the HTML report and confirm the "Active users" timeline shows a stable plateau rather than a sawtooth pattern.
To verify the result, check the statistics.json file or the HTML report. Ensure that the activeUsers count matches your closed(n) configuration. If the active user count spikes or dips significantly, check for scenario errors (e.g., 404s or timeouts) that may be causing users to terminate their sessions early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.