Avoiding False Bottlenecks with Gatling's Async Engine and Feeders
Thread-per-user load generators create false bottlenecks. Gatling’s async engine, injection profiles and feeders let you measure real p95/p99 latency without generator saturation.
06 Aug 2025, 10:20 UTC

Load tests often fail the load generator before they fail the system under test. With a thread-per-user model, simulating thousands of concurrent users means thousands of threads, each with its own stack. Context switching dominates CPU time and memory grows linearly, so the metrics you collect reflect generator exhaustion rather than application latency.
Gatling avoids that trap with an asynchronous, event-driven engine built on Akka and Netty. A small pool of threads manages thousands of concurrent connections. When a virtual user sends a request the thread is released until the response event resumes that user. That design keeps a single machine honest at high concurrency and preserves percentile accuracy.
Non-blocking I/O keeps the generator honest
In a synchronous framework, 5,000 waiting users require 5,000 threads. In Gatling, a virtual user is a lightweight session driven by events. The engine tracks state in the session and resumes execution on I/O completion. Because thread scheduling is not the bottleneck, response-time percentiles such as p95 and p99 are more likely to represent the system under test.
The practical check is simple. Run a ramp and inspect the generated HTML report under the global response time distribution. If the generator CPU stays well below saturation while percentiles rise, the signal is likely from the target. If generator CPU is at 100%, results are suspect.
Injection profiles shape realistic traffic
Real traffic arrives over time, not as an instant flood. Gatling’s Injection Profile DSL lets you describe arrival patterns.
- rampUsers: increase the number of users over a duration to observe warm-up and scaling.
- constantUsersPerSec: maintain a steady arrival rate for a steady state.
- atOnceUsers: inject a burst to test thundering-herd behavior.
Choosing a profile is an engineering decision. Ramp first to find a stable baseline, then hold constant to measure tail latency, then burst to test headroom.
Data-driven scenario with feeders and session variables
Caching bias is a common source of false negatives. Feeders stream unique data per iteration from CSV or JSON, and session variables carry extracted values forward.
import io.gatling.javaapi.core.*
import io.gatling.javaapi.http.*
import io.gatling.javaapi.core.Dsl.*
class SearchSimulation extends Simulation {
// Run from project root with Gatling installed. No special permissions required.
// Risk: feeder file must be readable by the process running the simulation.
val userFeeder = csv("data/users.csv").circular()
val httpProtocol = http.baseUrl("https://api.example.com")
.acceptHeader("application/json")
val scn = scenario("Search Scenario")
.feed(userFeeder)
.exec(http("Search Product")
.get("/search")
.queryParam("q", "#{searchTerm}")
.check(status.is(200))
.check(jsonPath("$.result").saveAs("result"))
)
.exec(session => {
// session variable "result" is available here for the next request
session
})
setUp(
scn.inject(
rampUsers(100).during(60),
constantUsersPerSec(5).during(300)
)
).protocols(httpProtocol)
}
The CSV data/users.csv must contain a header searchTerm. .circular() reuses rows when the feeder is exhausted. Verify feeder use by enabling Gatling’s console output and confirming each virtual user logs a distinct searchTerm value, and by checking the report for even distribution across unique queries.
Limitations to manage on the generator
Asynchronous efficiency does not remove OS limits. High concurrency can exhaust file descriptors or memory if the load generator is not tuned. Increase ulimit for open files and allocate sufficient heap for Gatling.
Because execution is non-blocking, stack traces are less intuitive than in synchronous frameworks. Keep simulation logic lean. Heavy CPU work such as complex JSON parsing or cryptography inside the scenario can bottleneck the generator itself and skew latency.
Check generator health during a run: monitor CPU, GC pauses, and open file descriptors on the machine running the simulation. If generator metrics degrade while target metrics stay flat, scale out generators or reduce per-user logic.
Start with a ramp, validate feeder uniqueness, and watch generator resource usage alongside p95/p99. That combination separates real performance issues from generator artifacts.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.