Gatling load testing means writing a simulation as code, in Java, Kotlin, Scala, JavaScript or TypeScript, that defines the protocol, the user journey and how fast users arrive, and running it with your build tool. With Maven that is mvn gatling:test, which runs the test, prints live statistics and writes an HTML report.
This tutorial builds one Java simulation from scratch, runs it against a small local shop API, and shows the output Gatling 3.16.0 printed, including the assertion that failed the build and the feeder setting that crashed a second run at 24 seconds.
Key Takeaways
- Gatling 3.16.0 (September 2026) is open source under Apache 2.0 and runs on 64-bit OpenJDK LTS versions 11 to 25.
- A simulation has three parts: an HTTP protocol, one or more scenarios, and an injection profile in
setUp. - Gatling fails a request on a 4xx or 5xx status even if you write no check. In our run that caught all 8 HTTP 500s.
- Assertions turn the run into a pass or fail: our 1.25% failed requests broke a “less than 1%” rule, and Maven exited with code 1.
- The default CSV feeder hands out each row once. When the rows ran out, Gatling crashed the run at 24 seconds.
What is Gatling?
Gatling is an open-source load testing tool that runs on the Java Virtual Machine, licensed under Apache 2.0, in which every test is a program called a simulation. You can write one in Java, Kotlin or Scala, and with a separate npm package in JavaScript or TypeScript. The current release is 3.16.0, published on 29 September 2026.
A simulation describes the protocol settings, the scenarios each virtual user follows, and the injection profile, and it sits in your test sources and runs as a Maven, Gradle or sbt goal. Gatling also sells Gatling Enterprise, a hosted edition with distributed load generators; this tutorial uses the free edition. If you are still choosing a tool, our k6 vs JMeter vs Gatling vs Locust comparison sets Gatling against the other three.
How do you install Gatling?
Gatling needs a 64-bit OpenJDK LTS release from 11 to 25, and the docs recommend 17 or 21. You then add Gatling to a project through a build tool. The docs recommend starting from their downloadable Java and Maven project, or you can add two entries to your own pom.xml.
Check Java and Maven first:
java -version
mvn -version
We used Java 17.0.20 and Maven 3.10.0. This is our test project’s pom.xml, trimmed, with the Gatling library as a test dependency and the Maven plugin:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>shop</groupId>
<artifactId>shop-load-test</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>3.16.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>4.21.12</version>
</plugin>
</plugins>
</build>
</project>
Simulations go in src/test/java and CSV files in src/test/resources. Avoid the milestone versions (with an M in the number) on Maven Central: the docs say they are not documented and are released only for internal use or Gatling Enterprise customers.
How do you write a Gatling simulation in Java?
A Gatling simulation is a Java class that extends Simulation. Inside it you build an HTTP protocol with http.baseUrl(...), one or more scenarios with scenario(...) chained to exec steps, and an initialiser block that calls setUp with each scenario’s injection profile, the protocol and any assertions.
Our test target is a small shop API with four endpoints: a home page, a product endpoint that takes 20 to 80 ms and fails with HTTP 500 about 2% of the time, a login that returns a token, and a cart that needs that token and now and then returns HTTP 200 without its items key. Here is the whole simulation, src/test/java/shop/ShopSimulation.java:
package shop;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
public class ShopSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http
.baseUrl("http://127.0.0.1:8080")
.acceptHeader("application/json");
// One row per virtual user; circular() reuses rows instead of running out
FeederBuilder<String> users = csv("users.csv").circular();
ScenarioBuilder browse = scenario("Browse")
.feed(users)
.exec(http("GET /").get("/"))
.pause(1)
.exec(http("GET /products").get("/products?id=#{productId}"));
ScenarioBuilder cart = scenario("Cart")
.feed(users)
.exec(
http("POST /login").post("/login")
.body(StringBody("{\"username\":\"#{username}\",\"password\":\"#{password}\"}"))
.asJson()
.check(jmesPath("token").saveAs("token")))
.exec(
http("GET /cart").get("/cart")
.header("Authorization", "Bearer #{token}")
.check(status().is(200), jmesPath("items").exists()));
{
setUp(
browse.injectOpen(
rampUsersPerSec(1.5).to(7.5).during(10),
constantUsersPerSec(7.5).during(50)),
cart.injectOpen(
rampUsersPerSec(0.5).to(2.5).during(10),
constantUsersPerSec(2.5).during(50)))
.protocols(httpProtocol)
.assertions(
global().responseTime().percentile(95.0).lt(200),
global().failedRequests().percent().lt(1.0));
}
}
What each part does:
http.baseUrl(...)sets the server for every request, andacceptHeaderadds a header to all of them.csv("users.csv")reads a file with a header row (username,password,productId) fromsrc/test/resources..feed(users)gives the user one row, and#{productId}puts a value into a URL or body.http("GET /products")names the request. Gatling reports statistics by this name, so every product ID lands in one row..pause(1)waits one second between the two browse requests.jmesPath("token").saveAs("token")reads the token from the login response and stores it in the user’s session for the cart request..check(status().is(200), jmesPath("items").exists())fails the cart request if the status is not 200 or the body has noitemskey.injectOpen(...)starts users at a rate per second. The two scenarios run side by side, three browse users for every cart user.assertions(...)set the pass or fail rules for the whole run.
How do you inject users in Gatling?
Gatling has two workload models and keeps them apart. injectOpen controls how fast new users arrive, in users per second, whatever the system does. injectClosed controls how many users are inside the system at once, starting a new one when another finishes. Each scenario uses one model; the docs say chained steps “must all be of the model kind”.
A public website or API, where visitors keep arriving however slow the server is, is an open system; a fixed pool of agents or devices is closed. Open injection also makes slowdowns visible: arrivals do not slow down with the server, so users pile up as they would in production.
| Model | Step | What it does |
|---|---|---|
| Open | atOnceUsers(n) | Starts n users at the same moment |
| Open | rampUsers(n).during(d) | Starts n users spread evenly over d |
| Open | constantUsersPerSec(r).during(d) | Starts r users per second for d |
| Open | rampUsersPerSec(r1).to(r2).during(d) | Raises the arrival rate from r1 to r2 over d |
| Open | stressPeakUsers(n).during(d) | Starts n users on a smooth S-shaped curve over d |
| Open | nothingFor(d) | Pauses injection for d |
| Closed | constantConcurrentUsers(n).during(d) | Keeps n users active for d |
| Closed | rampConcurrentUsers(n1).to(n2).during(d) | Moves the active count from n1 to n2 over d |
incrementUsersPerSec and incrementConcurrentUsers build stairs for capacity tests, and atOnceUsers with a large number is the quickest spike test. Our profile started 420 browse users and 140 cart users.
How do you run a Gatling test?
Run mvn gatling:test from the project folder. Maven compiles the simulation, Gatling runs it, prints a statistics block every five seconds, and writes an HTML report under target/gatling/ when it ends. With one simulation in the project it starts straight away; with several, it asks which one to run.
mvn gatling:test
In CI, the prompt for a simulation would hang the job, so the plugin skips it in several cases, including Maven’s batch mode (-B), the CI environment variable set to true, and a simulation named on the command line:
mvn -B gatling:test -Dgatling.simulationClass=shop.ShopSimulation
With more than one simulation and no class named, those modes fail instead of guessing. The live block shows requests so far as OK and KO (passed and failed), the errors, and users waiting, active and done per scenario.
How do you read Gatling results?
At the end Gatling prints a Global Information block and writes the HTML report, whose Stats table splits every number by request name. The Global block covers the whole run, with each figure split into OK (passed) and KO (failed) requests; read the per-request rows first and the error list second. This is ours, trimmed:
---- Global Information --------------------------------|---Total---|-----OK----|----KO----
> request count | 1,120 | 1,106 | 14
> mean response time (ms) | 21 | 21 | 24
> response time 50th percentile (ms) | 2 | 2 | 27
> response time 95th percentile (ms) | 76 | 76 | 62
> response time 99th percentile (ms) | 84 | 84 | 63
> mean throughput (rps) | 18.36 | 18.13 | 0.23
---- Errors ----------------------------------------------------------------------------------
> status.find.in([200, 209], 304), found 500 8 (57.14%)
> jmesPath(items).find.exists, found nothing 6 (42.86%)
And the per-request rows from the report’s Stats table, in milliseconds:
| Request | Total | KO | 50th pct | 95th pct | 99th pct | Max |
|---|---|---|---|---|---|---|
| GET / | 420 | 0 | 1 | 4 | 10 | 15 |
| POST /login | 140 | 0 | 1 | 5 | 7 | 9 |
| GET /products | 420 | 8 | 55 | 82 | 86 | 92 |
| GET /cart | 140 | 6 | 0 | 3 | 5 | 9 |
Four things to take from this output.
Read each request, not the Global row. The global median of 2 ms is real but tells you nothing: 700 of the 1,120 requests went to the fast home page, login and cart, which hid the product endpoint’s 55 ms median and 82 ms 95th percentile. Our guide to why percentiles beat averages explains why the tail is the number to watch.
Gatling caught the 500s without being asked. We wrote no check on the product request, yet all 8 HTTP 500s are KO. If a request has no explicit status check, Gatling applies an implicit one that the docs describe as “2XX or 304”; the error line shows it as status.find.in([200, 209], 304). Writing your own status check, as we did on the cart, replaces it.
The error list separates failure types. The 500s and the 6 cart responses that were HTTP 200 without items appear as two different lines. The second kind only shows up because of the jmesPath("items").exists() check: a status check alone would have passed them.
Throughput follows from the injection profile. 560 users making two requests each is the 1,120 requests, about 18 per second. The HTML report, at the index.html path Gatling prints, adds charts over time and one page per request.
How do you fail a build with Gatling assertions?
Assertions check statistics for the whole run after it ends, and if any one fails, the simulation fails and so does the Maven goal. Each assertion picks a scope (global(), forAll() for every request, or details("name") for one), a statistic such as response time or failed requests, a metric, and a condition.
Our run printed both results and stopped the build:
Global: 95th percentile of response time is less than 200.0 : true (actual : 76.0)
Global: percentage of failed events is less than 1.0 : false (actual : 1.25)
[ERROR] Failed to execute goal io.gatling:gatling-maven-plugin:4.21.12:test (default-cli)
on project shop-load-test: Gatling simulation assertions failed ! ForkException -> [Help 1]
Maven exited with code 1, which any CI system reads as a failed step. Global assertions share the blending problem from the results section: fast endpoints can hide a slow one inside a global percentile. For a strict gate, assert on the request that matters, for example details("GET /products").responseTime().percentile(95.0).lt(100), or use forAll() so every request must pass on its own.
What happens when a Gatling feeder runs out of data?
With no strategy set, a CSV feeder uses queue: each row goes to one virtual user, in file order, and is never reused. When the last row is gone, the next user that needs one makes Gatling crash the whole simulation. The queue strategy suits unique logins or one-time records, but only if the file holds a row for every user the test will start.
We saw it happen. Our 200-row users.csv fed both scenarios, and in a control run without .circular() the injection profile started more than 200 users. At 24 seconds, after 393 requests, Gatling stopped with:
Simulation crashed during injection
Feeder csv(users.csv) (defined at: shop.ShopQueueSimulation.<init>(ShopQueueSimulation.java:19)) crashed: feeder is now empty.
Maven reported Gatling failed. and exited with code 1. Pick the strategy for your data:
| Strategy | Rows reused | Order | Runs out |
|---|---|---|---|
queue() (default) | No | File order | Yes, the run crashes |
shuffle() | No | Random | Yes, the run crashes |
random() | Yes | Random | Never |
circular() | Yes | File order, then starts again | Never |
For logins that must stay unique, keep queue and create an account for every user the profile starts. For product IDs or search terms, random or circular is safer.
How do you run Gatling on more than one machine?
The free edition runs a simulation from one machine. In the Gatling documentation, spreading one test across several load generators, including splitting the injection profile and sharding feeder files between them, is a Gatling Enterprise feature. To go past one machine with the free edition, you run the simulation on each machine yourself.
One machine also tests from one place, so every request starts from the same network. Watch the load generator’s CPU during a big run: if it is saturated, response times in the report include the generator’s own delay, not only the server’s.
Running load tests without managing load generators
LoadFocus does not run Gatling simulations. It runs JMeter and k6 scripts, and tests built in the browser, from 25+ cloud regions, with no JVM or load generators to manage, and keeps a history of runs you can compare over time.
If your team already has JMeter test plans, uploading them to JMeter load testing in the cloud may be the shorter path. If writing tests in the same language as your application is why you chose Gatling, keeping Gatling is a sound choice; the Gatling alternatives page compares the options.
Frequently Asked Questions
Is Gatling free?
Yes. Gatling’s open-source edition is licensed under Apache 2.0 and installs as a Maven, Gradle or sbt dependency, or as an npm package for JavaScript. You pay only for the machines that run it. Gatling Enterprise, the hosted edition with distributed load generators and team features, is a separate paid product.
Which Java version does Gatling need?
Gatling supports 64-bit OpenJDK LTS versions from 11 to 25, and its documentation recommends version 17 or 21. Other JVMs, such as 32-bit builds or OpenJ9, are not supported. Gatling 3.16.0 ran on Java 17.0.20 with Maven 3.10.0 in our October 2026 test.
Can you write Gatling tests in JavaScript?
Yes. Gatling has a JavaScript and TypeScript SDK that installs with npm and runs with npx gatling run --simulation <name>, still on the JVM underneath. Some features are Java, Kotlin and Scala only; the documentation marks them “Not supported by Gatling JS”. Scenarios, checks, feeders and injection work the same way as in the Java SDK.
What does KO mean in Gatling?
KO is Gatling’s label for a failed request, and OK for a passed one. A request is KO when one of its checks fails, including the implicit status check that fails any response outside 2XX and 304. The report counts KO requests per request name and lists each distinct error message with its count.
Sources
- Gatling 3.16.0 and gatling-maven-plugin 4.21.12 release dates, Apache 2.0 licence: repo1.maven.org (io.gatling metadata and gatling-core POM), October 2026.
- Java versions, languages, milestone versions and JavaScript setup: docs.gatling.io, “Gatling installation”, October 2026.
- First Java simulation and recommended Java versions: docs.gatling.io, “Create your first Java-based simulation”, October 2026.
- Maven setup,
gatling:testand non-interactive mode: docs.gatling.io, “Gatling Maven Plugin”, October 2026. - Open and closed injection steps: docs.gatling.io, “Injection”, October 2026.
- Implicit status check: docs.gatling.io, “HTTP checks”, October 2026.
- Assertion scopes and failure: docs.gatling.io, “Assertions”, October 2026.
- Feeder strategies and file locations: docs.gatling.io, “Feeders”, October 2026.
- Gatling Enterprise load generators, injection sharding and
shard()feeders: docs.gatling.io, “Injection”, “Feeders” and “Simulation”, October 2026. - Test runs: Gatling 3.16.0 on Java 17.0.20 and Maven 3.10.0, 60 seconds (browse 1.5 to 7.5 users per second, then 7.5; cart 0.5 to 2.5, then 2.5) against a local Python test API, plus a control run with the default
queuefeeder and 20-second constant phases, 2026-10-27.