Controlling Request Rates in Gatling: Injection Profiles vs. Throttling
Learn how to choose between injection profiles, throttling, and pacing in Gatling to maintain a steady target RPS without distorting latency measurements.
03 Jun 2026, 21:40 UTC

The Challenge of Target RPS
When performance testing an API, the goal is often to hit a specific target of Requests Per Second (RPS) to verify a Service Level Agreement (SLA). However, simply injecting a certain number of users does not guarantee a specific RPS. Because RPS is a function of both user arrival and the system's response time (RPS = Concurrent Users / Average Response Time), a system that speeds up or slows down during a test will cause your actual load to fluctuate, even if your injection profile remains constant.
The core decision is whether to control the arrival rate (how many users start) or the throughput rate (how many requests are actually sent). Choosing the wrong mechanism can lead to "coordinated omission," where the test tool hides system saturation by slowing down its own request rate.
Comparing Load Control Strategies
| Method | Primary Control | Behavior on Latency Spike | Best Use Case |
|---|---|---|---|
| Injection Only | User Arrival Rate | RPS drops as response times increase | Open-workload modeling (e.g., new users hitting a site) |
| Throttling | Request Ceiling | RPS stays capped; requests may be queued/rejected | Hard RPS limits for CI/CD or protecting SUT |
| Pacing | Inter-request Delay | RPS drops significantly as response times increase | Simulating human "think time" and per-user behavior |
Trade-offs and Engineering Risks
Injection Profiles model an "open system." You define how many users enter the system per second. If the System Under Test (SUT) slows down, the number of concurrent users will climb because they are arriving faster than they are finishing. This is the most realistic way to find the breaking point of a system.
Throttling acts as a governor. It ensures that Gatling never exceeds a specific RPS, regardless of how many users are injected. While this protects the SUT from being overwhelmed, it introduces a risk: if the SUT slows down, Gatling may queue requests internally. This can artificially inflate the measured response time because the timer starts when the request is scheduled, not necessarily when it hits the wire.
Pacing (using pause or custom pacing logic) creates a "closed system." Each user waits a fixed amount of time between requests. If the SUT response time increases, the total cycle time (Response Time + Pause) increases, which naturally lowers the overall RPS. This makes it very difficult to maintain a steady target load during a performance degradation.
Implementation: Combining Injection and Throttle
To achieve a reproducible steady-state test with a hard cap, combine an open-model injection profile with a throttle. This ensures you ramp up to your target load but never exceed the safety limit of your environment.
The following example uses the Gatling 3.x Scala DSL. Run this simulation using gatling:testOnly from your terminal with the necessary project permissions.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class LoadTest extends Simulation {
val httpProtocol = http.baseUrl("https://api.example.com")
val scn = scenario("API Load Test")
.exec(http("Request").get("/health"))
setUp(
// 1. Define the arrival rate (Open Model)
scn.inject(
rampUsersPerSec(10).to(100).during(2.minutes),
constantUsersPerSec(100).during(10.minutes)
)
).protocols(httpProtocol)
// 2. Enforce the throughput ceiling (The Governor)
.throttle(
reachRps(100).in(2.minutes),
holdFor(10.minutes)
)
}
Validating the Results
After the simulation completes, do not rely solely on the "Mean Response Time." Use the following checks in the HTML report to verify the load behavior:
- Offered vs. Achieved RPS: Compare the target RPS defined in the
throttlemethod against the actual "Requests per second" chart. If the achieved RPS is significantly lower than the offered RPS, the SUT is likely the bottleneck. - Active Users: Check the "Active Users" graph. If you used
constantUsersPerSec(100)but the active user count continues to climb linearly, the SUT is saturating and cannot clear the queue. - Throttle Rejections: Inspect the simulation logs for throttle-related warnings. If Gatling is rejecting requests because the throttle limit was hit, your injection profile is too aggressive for your throttle settings.
Limitations and Rollback
Throttling is an engine-level constraint. It does not change the behavior of the SUT; it only changes how Gatling sends traffic. If you find that your latency metrics are skewed by internal queuing, remove the .throttle() call and adjust your inject() profiles to reach the target RPS based on observed average response times.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.