Resource library

QA How-To

Gatling Injection Profiles Explained: Open vs Closed Models

Gatling injection profiles explained with runnable Java examples. Compare open arrival rates and closed concurrency, verify reports, and choose the right model.

19 min read | 3,290 words

TL;DR

An open Gatling profile schedules new virtual users by arrival rate with injectOpen. A closed profile maintains a target number of active virtual users with injectClosed. Choose according to whether real arrivals can continue during slowdowns or must wait for a free slot.

Key Takeaways

  • Use injectOpen when new users arrive independently of how long existing users take to finish.
  • Use injectClosed when a fixed population or real admission cap limits concurrent users.
  • constantUsersPerSec sets starts per second; it does not set request rate or active users.
  • constantConcurrentUsers replaces completed journeys to maintain a concurrency target.
  • Compare user starts, active users, response time, and failures together before interpreting a run.
  • Keep open and closed steps in separate injection profiles and validate the chosen model against production evidence.

Gatling Injection Profiles Explained means choosing the load pattern that matches how people enter your system. Use an open model when arrivals continue regardless of current response time, and a closed model when the number of active users is genuinely capped. The distinction changes what happens precisely when the application slows, so it can change your conclusion about capacity.

This tutorial builds two small Java simulations against Gatling's public ecommerce demo API. You will run each separately, inspect user start and active user charts, and learn to translate production evidence into the right profile. If you need the broader tool setup first, read Gatling basics for testers.

TL;DR

Question Open profile Closed profile
What do you set? New virtual users per second Concurrent active virtual users
Main Gatling method injectOpen(...) injectClosed(...)
What happens when journeys slow? Arrivals continue; concurrency can rise Replacements slow; arrival rate can fall
Typical fit Public API or website with independent arrivals Worker pool, licensed seats, or enforced admission cap
Chart to check first Users started per second Active users

Do not treat these as two spellings of the same load. Ten new journeys each second can create far more than ten simultaneous users if each journey takes several seconds. Conversely, ten concurrent users can generate very different arrival rates as journey duration changes. Gatling's workload model guidance and injection reference define these controls explicitly.

What You Will Build

  • A Java OpenProfileSimulation that ramps from zero to two user starts per second, then holds that arrival rate.
  • A Java ClosedProfileSimulation that ramps from zero to four concurrent users, then maintains four users.
  • A repeatable command for each simulation, with separate HTML reports and a concrete comparison checklist.
  • A decision rule for adapting either small demonstration to an authorized staging environment.

The numbers are deliberately illustrative and low. They teach the profile semantics; they are not capacity targets or claims about the demo service. One virtual user performs one GET request to /session, checks for HTTP 200, and ends. This makes the relationship between journey duration and active users easier to observe. A production journey usually includes more requests and pauses, so its duration and resulting concurrency will differ.

Prerequisites

Use 64-bit OpenJDK 17 and the Maven Wrapper supplied with Gatling's official Java/Maven demo project. Gatling's Java tutorial uses Java 17 for its sample and documents the wrapper workflow. Use the Gatling and Maven plugin versions already declared in that project's pom.xml; if you transfer the code into another project, match its installed Gatling version instead of copying an arbitrary pin. You also need Git, a terminal, and permission to send a small amount of traffic to the target.

java -version
git --version

Verify: java -version identifies a 17.x runtime. The next step checks the project's Maven Wrapper version. If your terminal resolves a different JDK through JAVA_HOME, correct that before diagnosing Java compilation errors. The official demo host is suitable for this small exercise; use your own nonproduction environment for sustained or higher load.

Step 1: Get the Java/Maven Starter

Clone Gatling's ecommerce demo tests and move into its Java/Maven module. The starter provides a compatible pom.xml, wrapper, and conventional src/test/java/example location. Keep this module as the working directory for every command below. A normal application Vite or React project is not a substitute for a Gatling Maven module.

git clone https://github.com/gatling/se-ecommerce-demo-gatling-tests.git
cd se-ecommerce-demo-gatling-tests/java/maven
./mvnw -version

Verify: Maven prints its runtime version and Java 17, and test -f src/test/java/example/BasicSimulation.java succeeds. If the wrapper cannot execute on a Unix shell, run chmod +x mvnw and retry. On Windows, use mvnw.cmd in subsequent commands. Inspect pom.xml before updating dependencies: the starter already pairs Gatling libraries with its plugin.

The starter can contain multiple simulations. That is why later commands pass -Dgatling.simulationClass=...; otherwise the plugin may prompt you to choose one. This command-line property is documented in the Gatling Maven plugin reference. Do not edit the starter's existing test for this exercise. The two new files allow you to inspect each load model without accidentally blending metrics from concurrent scenarios.

Step 2: Gatling Injection Profiles Explained Through Open Arrivals

Create src/test/java/example/OpenProfileSimulation.java with this complete Java class. The HTTP base URL and /session endpoint follow Gatling's own Java demo. The status check makes an unexpected response a failed request rather than a misleadingly green run. rampUsersPerSec schedules starts, then constantUsersPerSec maintains the specified start rate for the hold. during(20) means seconds in the Java DSL.

package example;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import io.gatling.javaapi.http.HttpProtocolBuilder;

public class OpenProfileSimulation extends Simulation {
  HttpProtocolBuilder httpProtocol = http
      .baseUrl("https://api-ecomm.gatling.io")
      .acceptHeader("application/json");

  ScenarioBuilder sessionJourney = scenario("Open session journey")
      .exec(http("GET session")
          .get("/session")
          .check(status().is(200)));

  {
    setUp(sessionJourney.injectOpen(
        rampUsersPerSec(0).to(2).during(20),
        constantUsersPerSec(2).during(40)
    ).protocols(httpProtocol));
  }
}

Verify: compile the new class without sending load: ./mvnw test-compile. A successful Maven build proves that the imports, package, DSL method names, and project dependencies align. The class filename must match the public class name exactly. If the clone placed the sample in a different package, preserve the example package path shown here or adjust the package and later fully qualified simulation name together.

An open profile controls starts, not completions. During the 40-second hold, the illustrative target is two new users each second. Since each user issues one request, request rate may look similar in this tiny example, but that coincidence disappears when a journey sends multiple requests or retries. constantUsersPerSec(2) does not mean two active users, and it does not guarantee exactly two completed requests each second. Scheduling, network latency, and failures can change observed completions.

Step 3: Run and Inspect the Open Profile

Launch only the class you just wrote. The Maven plugin writes an HTML report below target/gatling/ and prints its exact path at the end of the run. Allow the run to finish before reading percentiles or global counts; partial console output is not the final report.

./mvnw gatling:test -Dgatling.simulationClass=example.OpenProfileSimulation

Verify: Maven reports a completed simulation and a report location. Open that run's index.html, then find the users started per second and active users charts. The planned profile is a 20-second arrival-rate ramp followed by a 40-second hold. You should see starts rise toward the target and then remain around it, subject to chart sampling. Active users are an outcome, not a configured target; for a one-request journey they may be low and noisy.

Think through the arithmetic before comparing charts. If ten users arrive each second and each journey lasts one second, roughly ten can be active on average under steady conditions. If the same journeys take five seconds while arrivals remain ten per second, roughly fifty can be active. This is an illustrative form of Little's Law, concurrency ~= arrival rate x average journey duration, which assumes a reasonably stable observation window. It is not an assertion that this demo will produce those numbers. Failed journeys, changing arrival rates, long tails, and startup periods complicate the estimate.

Open traffic is useful for testing a public endpoint that continues receiving attempts during a slowdown. It can reveal the queueing and latency growth that a fixed user pool may hide. It can also overwhelm the load generator if you choose rates above its capacity, so monitor the generator as well as the server before claiming the application saturated. The load testing guide provides broader context for shaping traffic and interpreting bottlenecks.

Step 4: Gatling Injection Profiles Explained Through Closed Concurrency

Add a second complete file, src/test/java/example/ClosedProfileSimulation.java. Keep the same endpoint and request check so the controlled variable is the injection model. In the closed model, Gatling starts replacement users as earlier users finish to hold the target number of active virtual users during the constant phase. A user in this sample exits after the /session response and status check.

package example;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import io.gatling.javaapi.http.HttpProtocolBuilder;

public class ClosedProfileSimulation extends Simulation {
  HttpProtocolBuilder httpProtocol = http
      .baseUrl("https://api-ecomm.gatling.io")
      .acceptHeader("application/json");

  ScenarioBuilder sessionJourney = scenario("Closed session journey")
      .exec(http("GET session")
          .get("/session")
          .check(status().is(200)));

  {
    setUp(sessionJourney.injectClosed(
        rampConcurrentUsers(0).to(4).during(20),
        constantConcurrentUsers(4).during(40)
    ).protocols(httpProtocol));
  }
}

Verify: run ./mvnw test-compile again. This compiles both classes but runs neither simulation. In a code review, check that injectClosed contains only closed steps and that the open class contains only open steps. Gatling does not allow mixing open and closed steps within one injection profile. Separate simulations here also prevent an accidental simultaneous run from making the reports hard to interpret.

The closed profile is appropriate when real admission controls limit the active population. Think of four agents who can each work one case at a time. A fifth case waits until an agent finishes. If each agent becomes slower, the number of active agents remains four but completed work per second falls. A public API with no such admission gate would continue receiving new attempts. Modeling that API as four concurrent users would quietly reduce incoming load as latency grows and could understate its failure mode.

Step 5: Run the Closed Profile Separately

Execute the second class using the same project and Java runtime. Do not compare raw request totals as though the two profiles applied equivalent load. One is specified in users per second and the other in simultaneous users, so equal-looking integers have different units. The endpoint and journey are held constant only to make the mechanics visible.

./mvnw gatling:test -Dgatling.simulationClass=example.ClosedProfileSimulation

Verify: open the new report from target/gatling/ and locate active users. It should rise toward four during the ramp and remain near four during the hold, except for sampling boundaries and brief replacement gaps. Then inspect users started per second. That chart is a consequence of how quickly the one-request journeys finish; it is not supposed to form a flat line at four. If the service slows or requests time out, replacements are initiated less frequently.

Gatling's timings reference distinguishes user start rate from concurrent users. Use those charts with response times and failure counts. A closed test can appear stable in concurrent users even while throughput collapses. That is exactly why an apparently flat active-user line is insufficient evidence of capacity. Also note the lifecycle boundary: constantConcurrentUsers(4).during(40) maintains a population for the duration; it does not promise exactly four completed requests per second or four total users in the whole run.

A realistic closed journey normally contains multiple actions and pauses. If you add them later, keep the action sequence identical in a paired comparison. Any difference in path length changes the replacement rate. For designs with logins, unique customer data, or stateful purchases, use isolated test accounts and cleanup. The Gatling scenario design guide covers shaping a representative journey before tuning the injection profile.

Step 6: Compare Charts and Explain the Difference

Write down the units before reading any headline number. For each report, record the configured target, measured starts per second, measured active users, response-time distribution, and failed-request count. Compare the same hold window, not the full run's aggregate, because the ramp contributes lower load. Gatling's static report exposes time-series charts, while its global summary combines the phases. If a report has low sample volume, do not overinterpret percentile differences.

Observation during slowdown Open run Closed run
Scheduled new users Continues at configured rate Depends on departures
Active users Can grow as journeys last longer Tracks configured cap
Request completion rate Can lag arrivals or collapse Can fall as users occupy slots longer
Useful capacity question Can the service absorb continuing demand? Can a fixed population complete work acceptably?

Verify: place the two generated index.html report paths in your test notes and confirm the filenames or report headers identify the correct simulation. Do not average the two reports together. If the report offers a request chart for GET session, compare that named request rather than a global chart that might include unrelated calls from a later, larger scenario. The CLI command find target/gatling -name index.html -print lists the generated reports without assuming a timestamped directory name.

A subtle point is that user starts are not necessarily request starts. A scenario can authenticate, browse, pause, and submit several HTTP calls. The user start chart describes admission into the scenario, while requests per second reflects all executed requests. In this one-request tutorial they are close enough to teach the model, but you must separate them before making production claims. The API performance testing tutorial develops checks and measurements for longer API flows.

Step 7: Choose a Profile from Production Evidence

Start with a question about admission: can a new customer enter while existing customers are waiting? If yes, map measured arrivals, such as successful ingress requests or session creations per minute, to an open model. If a queue, semaphore, seat license, or bounded worker pool truly prevents entrance until a slot opens, map observed active users to a closed model. Document where the cap is enforced. A database connection pool inside an otherwise public API is an internal bottleneck; it does not by itself make customer arrivals closed.

For an open workload, choose a rate from real time-series data, not a peak concurrent-session count. Separate weekday baseline, known campaigns, and bursts. Reproduce their duration and ramp shape. If you know the hourly arrivals, divide by 3600 only as a starting average; minute-level peaks can be much higher. Use rampUsersPerSec for a rising arrival rate, constantUsersPerSec for a plateau, and atOnceUsers only when an actual batch-like burst is the question. Gatling also provides incrementUsersPerSec for staircase experiments, but each level still represents an arrival rate.

For a closed workload, first establish the number of effective slots in production. Choose rampConcurrentUsers to reach the cap and constantConcurrentUsers to maintain it. Distinguish a technical cap from a business operating level: a pool of 200 workers does not imply all 200 are normally busy. A descending concurrency ramp also does not kill existing journeys; users end when their scenario ends. Schedule enough time for completion and compare outcomes in the intended steady window. Gatling's injection reference documents this ramp-down behavior.

Verify: write one sentence in your test plan naming the real admission mechanism and one cited production metric with its unit. For example, new sessions/minute at the edge supports an open profile; occupied processing slots supports a closed one. If you cannot identify either, observe production traffic before scaling the demo numbers. This is the judgment an interviewer looks for in performance testing interview questions.

Step 8: Add a Pass/Fail Check Without Confusing It With Load

The .check(status().is(200)) in each class validates every response. For an automated run, add a simulation-wide assertion to one class after the .protocols(httpProtocol) call. In this example, the allowed failed request count is zero. Gatling evaluates global assertions after the run, so the Maven goal can fail even if the injector completed its schedule. Keep this threshold separate from the injection setting: an assertion judges results; it does not change the rate or concurrency target.

setUp(sessionJourney.injectOpen(
    rampUsersPerSec(0).to(2).during(20),
    constantUsersPerSec(2).during(40)
).protocols(httpProtocol))
    .assertions(global().failedRequests().count().is(0L));

Replace the setUp(...) statement inside OpenProfileSimulation with the complete statement above, then run ./mvnw gatling:test -Dgatling.simulationClass=example.OpenProfileSimulation. Verify: the console prints an assertion result and Maven exits successfully only if no request is classified as failed. To enforce the same rule on the closed class, keep its injectClosed(...) block and append .assertions(global().failedRequests().count().is(0L)) to its setup. The Gatling assertions reference documents this Java DSL and explains that assertions are evaluated after the simulation.

A zero-failure rule is useful for this small GET example. Real service objectives may permit a bounded error percentage and specify a response-time percentile, but choose those thresholds from your product's SLO and a representative test window. Never declare a performance pass merely because Gatling generated an HTML report. A report can contain failed checks, high latency, or an injector that never achieved the intended rate. Keep the raw report and the workload definition with the run so a reviewer can reproduce the claim.

Troubleshooting

  • Problem: java -version and Maven show different runtimes. -> Inspect JAVA_HOME and ./mvnw -version; align both to the supported JDK used by the starter before retrying compilation.
  • Problem: Maven asks which simulation to run. -> Supply the fully qualified -Dgatling.simulationClass=example.OpenProfileSimulation or the corresponding closed class. Check the package line if the plugin cannot find it.
  • Problem: A request fails with an unexpected status. -> Confirm the demo endpoint is reachable, inspect the named GET session result, and verify the response contract before changing the check. Do not turn a failing response into a passing test by removing validation.
  • Problem: Open active users do not equal two. -> Two is the configured starts per second, not concurrent users. Inspect journey duration and user starts; multiply a stable average duration by arrivals only as an estimate.
  • Problem: Closed starts per second are uneven. -> Four is the active-user target. Short journeys cause frequent replacements; slow or failing journeys change that replacement pace. Compare active users, latency, and failures in the same time interval.
  • Problem: The report is missing or incomplete. -> Let the run finish, use the exact report path printed by Maven, and check target/gatling/. A compilation failure or interrupted run may prevent a final HTML report.

Interview Questions and Answers

Practice these questions with the two reports open. State the controlled variable, then explain what happens when the application slows. The JSON interview answers below give fuller model responses; these prompts focus on the measurements you should point to while speaking.

Q: What is the difference between injectOpen and injectClosed?

injectOpen controls user starts over time, while injectClosed controls the active-user population. I would demonstrate the difference with starts-per-second and active-user charts from separate runs.

Q: Does constantUsersPerSec(10) mean ten concurrent users?

No. It schedules ten new virtual users each second. Concurrency depends on how long those users remain in their scenario.

Q: Why can a closed test understate risk for a public API?

When response time rises, occupied slots release more slowly, so a closed injector admits fewer new users. Real callers may continue arriving and build a queue instead.

Q: What does a constant-concurrency plateau prove?

It proves the injector maintained roughly the requested active population. It does not prove acceptable throughput, latency, or error rate; those need separate checks.

Q: Can you mix open and closed steps in one profile?

No. Each scenario's injection profile uses one model. Use separate simulations or thoughtfully separate scenarios if you truly have different admission mechanisms.

Q: Which Gatling chart validates an open profile?

Start with users started per second, then inspect active users, request rate, and failures to see how the system responded to those arrivals.

Common Mistakes

  • Calling a user-start rate requests per second when each scenario performs several requests.
  • Copying a concurrency number from monitoring into constantUsersPerSec without converting units or examining session duration.
  • Using a fixed active-user pool for traffic that keeps arriving during an incident.
  • Comparing whole-run averages while one profile spent much of the run ramping.
  • Running both sample simulations at once and attributing shared server behavior to only one model.
  • Raising load on a public demo or production system without authorization and generator monitoring.
  • Removing response checks after failures, then interpreting successful injection as application success.

Where To Go Next

Replace the demo GET with one representative journey on a staging system you are allowed to test. Keep the workload model choice tied to observed admission behavior, add request checks, and make the hold long enough to see stable behavior. For more complex arrival shapes, follow Gatling scenario design and Gatling load testing a GraphQL API. For a tool comparison after you understand the workload units, see JMeter vs Gatling.

Conclusion

Gatling Injection Profiles Explained is fundamentally a choice of controlled variable. An open profile controls how many users begin a journey each second; a closed profile controls how many are active at once. Run the two examples separately, verify the chart that corresponds to each configured target, and interpret response time and failures alongside it.

For your next real test, identify the production admission rule and record its measured unit before editing the injection numbers. That small step prevents a plausible-looking report from answering the wrong capacity question.

Interview Questions and Answers

How would you explain Gatling open and closed injection in an interview?

Open injection controls the rate at which virtual users start. Closed injection controls how many virtual users remain active. I choose between them by asking whether production admits new users while earlier users are still waiting. I then verify the appropriate start-rate or active-user chart and examine latency and failures separately.

Why is constantUsersPerSec not a concurrency setting?

Its unit is new users per second. Concurrent users are an outcome of arrival rate and journey duration, so a slowdown can increase concurrency even when the configured arrival rate stays unchanged. I would use the active-user chart to observe that effect rather than infer it from the DSL argument.

How does constantConcurrentUsers maintain its target?

It starts replacement virtual users as existing journeys finish, aiming to hold the active population at the configured number for the step duration. If journeys take longer, replacements occur less often. Therefore a stable active-user count can coexist with lower throughput.

Which workload model fits an uncapped public API?

Usually an open model, because callers can keep arriving regardless of current response time. I would derive an arrival-rate profile from ingress or session-start telemetry and include peak windows. A closed model might hide queue growth by slowing replacement users when the API degrades.

What evidence would make you choose a closed model?

I would look for a real admission boundary, such as a fixed operator pool, licensed seat limit, or queue that only admits a new job when a slot frees. I would measure occupied slots and queue behavior during representative periods. A fixed thread or connection pool inside the service is not enough if external requests still arrive.

Can I put open and closed steps inside one inject call?

No. Gatling requires the steps within one scenario injection profile to belong to one model. I would create separate profiles for distinct experiments, or separate scenarios when the system actually has distinct traffic populations, and document why each population has its model.

How do you validate that an open injection profile actually ran as intended?

I inspect users started per second against the ramp and hold I configured, then check active users and the request chart. If the start rate diverges materially, I investigate generator capacity, configuration, and run completion. I keep response checks and failure counts in the same report so an achieved rate is not mistaken for healthy service.

Why can two profiles with the same numeric value produce different traffic?

The units differ: ten users per second is a flow, while ten concurrent users is a population. The closed profile's replacement rate depends on journey duration; the open profile's concurrency depends on that duration. I would never call them equivalent based on the number ten alone.

What happens when a closed concurrency ramp goes downward?

Gatling does not terminate virtual users in flight. Users complete their scenarios naturally, and new starts are reduced to approach the lower target. I allow enough time for that transition and avoid assuming the active-user chart will drop instantaneously.

Frequently Asked Questions

What is an injection profile in Gatling?

It is the sequence of steps that schedules virtual users for a scenario. An open profile sets arrivals over time; a closed profile sets the active-user population. The scenario itself defines what each user does after starting.

When should I use injectOpen?

Use it when new users can arrive independently of how slowly current users are served, as on most public websites and APIs. Base the rate and shape on measured production arrivals, then confirm the user-start chart in the report.

When should I use injectClosed?

Use it when admission truly waits for a free slot, such as a fixed worker population or enforced session cap. Confirm the cap exists in the real system; an internal database pool alone does not cap external arrivals.

Does constantUsersPerSec control HTTP requests per second?

No. It schedules new virtual users per second. One virtual user can issue one request, many requests, or none if its scenario exits early, so request throughput must be read separately.

Does a closed profile stop users during a downward ramp?

No. Gatling does not interrupt active users merely because the configured concurrency target declines. Existing users finish their scenarios, and the injector adjusts new starts toward the new target.

Can open and closed injection steps be combined in one scenario profile?

No. A single injection profile must contain steps of one workload model. Keep the two examples separate so each report is attributable to one model.

Why does a closed test show fewer starts when response time rises?

Active users remain occupied longer, so fewer finish and free a slot for a replacement. The target concurrency can remain flat while throughput and arrivals decline.

Related Guides