Gatling API Load Testing: A Quickstart Guide for Developers
Gatling is a high-performance open-source load testing tool built on Akka and Netty that lets developers write realistic API simulations in Java, Kotlin, or Scala. This guide walks you from zero to a running load test in under 30 minutes, covering Maven setup, your first simulation, and how to read the output.
Key Takeaways
- Gatling uses a code-first DSL — simulations are real code, not YAML or XML, so they live in version control and benefit from refactoring tools.
- A simulation has three parts: an HTTP protocol definition, one or more scenarios (chains of requests), and an injection profile (how users ramp up).
- Injection profiles control load shape —
rampUsers,constantUsersPerSec, andatOnceUserscover the most common patterns. - The HTML report is generated automatically after every run — no extra setup needed to get percentile charts and request breakdowns.
- Threshold assertions let you fail fast — declare p95 < 500ms directly in the simulation file and Gatling exits with a non-zero code if the SLO is breached.
Load testing is one of those engineering disciplines that teams delay until something breaks in production. Gatling removes the excuse: it is fast to set up, produces publication-quality HTML reports, and scales to millions of requests per second on commodity hardware. This guide gets you to a working API load test from scratch.
What Is Gatling?
Gatling is an open-source load and performance testing framework originally written in Scala. Modern versions support Java and Kotlin as first-class languages, which means most teams can adopt it without touching Scala at all.
Under the hood, Gatling is asynchronous. Each virtual user is not a thread — it is a state machine managed by the Akka actor system running on top of Netty's non-blocking I/O. A single machine can drive tens of thousands of concurrent virtual users without the thread-per-user overhead that plagues older tools like Apache JMeter. This architecture is the main reason Gatling is preferred for API load testing at scale.
The tool has two editions:
- Gatling OSS — free, self-hosted, everything covered in this guide.
- Gatling Enterprise — adds distributed load generation, real-time dashboards, and hosted storage for run history. Worth evaluating if you need multi-region tests or long-term regression tracking.
Setting Up a Maven Project
The fastest way to get started is the official Maven archetype. Run this in a terminal:
mvn archetype:generate \
-DarchetypeGroupId=io.gatling.highcharts \
-DarchetypeArtifactId=gatling-highcharts-maven-archetype \
-DarchetypeVersion=3.11.5 \
-DgroupId=com.example \
-DartifactId=my-load-tests \
-Dversion=1.0-SNAPSHOT \
-DinteractiveMode=falseThis generates a project with the correct directory structure:
my-load-tests/
├── pom.xml
└── src/
└── test/
├── java/
│ └── com/example/
│ └── BasicSimulation.java
└── resources/
├── gatling.conf
└── logback-test.xmlOpen pom.xml and verify the Gatling Maven plugin is present:
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>4.9.6</version>
</plugin>If you prefer Gradle, add this to build.gradle:
plugins {
id 'io.gatling.gradle' version '3.11.5'
}
dependencies {
gatling 'io.gatling.highcharts:gatling-charts-highcharts:3.11.5'
}Writing Your First Simulation
Delete the generated BasicSimulation.java and create a new file at src/test/java/com/example/ApiLoadTest.java:
package com.example;
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
public class ApiLoadTest extends Simulation {
// 1. HTTP Protocol — base URL and common headers
HttpProtocolBuilder httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.userAgentHeader("Gatling/3.11");
// 2. Scenario — the chain of requests each virtual user executes
ScenarioBuilder scenario = scenario("Browse Products")
.exec(
http("List Products")
.get("/v1/products")
.check(status().is(200))
.check(jsonPath("$[0].id").saveAs("productId"))
)
.pause(1, 3) // think time: 1–3 seconds random pause
.exec(
http("Get Product Detail")
.get("/v1/products/#{productId}")
.check(status().is(200))
.check(responseTimeInMillis().lt(500))
);
// 3. Injection profile — how virtual users are launched
{
setUp(
scenario.injectOpen(
nothingFor(5), // warm-up pause
rampUsers(50).during(30), // ramp 50 users over 30 seconds
constantUsersPerSec(10).during(60) // sustain 10 users/sec for 1 minute
)
).protocols(httpProtocol)
.assertions(
global().responseTime().percentile(95).lt(500),
global().failedRequests().percent().lt(1.0)
);
}
}Understanding the Three Parts
HTTP Protocol declares defaults that apply to every request in the simulation. Setting baseUrl here means your individual get() and post() calls only need the path, not the full URL. Add shared headers — Authorization, Accept, Content-Type — here rather than repeating them per request.
Scenario is a ScenarioBuilder that chains exec() blocks. Each exec() contains an HTTP request built with http("name").get(path). The string name appears in your HTML report — make it meaningful. .check() calls validate the response: status code, JSON path extraction, response time. The saveAs("productId") pattern stores a value from one response into the virtual user's session so the next request can reference it with #{productId}.
Injection profile controls the load shape. rampUsers(50).during(30) adds one new virtual user every 600ms for 30 seconds. constantUsersPerSec(10).during(60) maintains a steady arrival rate of 10 new users per second. These two combined give you a ramp-up followed by a steady-state phase — the most common pattern for capacity testing.
Common Injection Profiles
Gatling's open model (new users arrive over time) and closed model (fixed pool of concurrent users) address different testing goals.
// Open model — arrival rate
rampUsers(100).during(60) // 100 users arrive over 1 minute
constantUsersPerSec(5).during(120) // 5 new users per second for 2 minutes
atOnceUsers(500) // spike: all 500 users start simultaneously
// Closed model — fixed concurrency
constantConcurrentUsers(50).during(60) // always exactly 50 users active
rampConcurrentUsers(10).to(100).during(60) // grow pool from 10 to 100 over 1 minuteUse the open model when you want to test a specific arrival rate (e.g., your Black Friday projection). Use the closed model when you want to test how the system behaves under a fixed concurrency level.
Running the Test
With Maven:
mvn gatling:test -Dgatling.simulationClass=com.example.ApiLoadTestWith Gradle:
./gradlew gatlingRun-com.example.ApiLoadTestGatling prints a live summary to the console every few seconds:
================================================================================
2026-05-31 14:23:45 5s elapsed
---- Requests ------------------------------------------------------------------
> Global (OK=142 KO=0 )
> List Products (OK=71 KO=0 )
> Get Product Detail (OK=71 KO=0 )
---- Browse Products -----------------------------------------------------------
[##########################################################################]100%
waiting: 0 / active: 50 / done: 0
================================================================================When the run finishes, Gatling writes the HTML report to target/gatling/apiloadtest-{timestamp}/index.html. Open it in a browser.
Reading the Console Output
The final summary looks like this:
================================================================================
---- Global Information --------------------------------------------------------
> request count 8520 (OK=8520 KO=0)
> min response time 12 (OK=12 KO=-)
> max response time 843 (OK=843 KO=-)
> mean response time 78 (OK=78 KO=-)
> std deviation 91 (OK=91 KO=-)
> response time 50th percentile 54 (OK=54 KO=-)
> response time 75th percentile 89 (OK=89 KO=-)
> response time 95th percentile 312 (OK=312 KO=-)
> response time 99th percentile 587 (OK=587 KO=-)
> mean requests/sec 94.7 (OK=94.7 KO=-)
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 8503 ( 99%)
> 800 ms <= t < 1200 ms 17 ( 0%)
> t >= 1200 ms 0 ( 0%)
> failed 0 ( 0%)
================================================================================The p95 of 312ms is well inside the 500ms assertion. The p99 of 587ms is worth noting but doesn't breach any threshold. The distribution table confirms 99% of requests finished in under 800ms.
Adding POST Requests and Request Bodies
Most APIs need more than GET requests. Here is how to send a JSON body:
.exec(
http("Create Order")
.post("/v1/orders")
.body(StringBody(
"""
{
"productId": "#{productId}",
"quantity": 1,
"customerId": "#{customerId}"
}
"""
)).asJson()
.check(status().is(201))
.check(jsonPath("$.orderId").saveAs("orderId"))
)For larger or more complex bodies, store the JSON template in src/test/resources/bodies/create-order.json and reference it:
.body(RawFileBody("bodies/create-order.json")).asJson()Targeting Specific Environments
Hard-coding the base URL makes simulations hard to reuse. Pass the target environment via a system property:
String baseUrl = System.getProperty("baseUrl", "https://staging.api.example.com");
HttpProtocolBuilder httpProtocol = http
.baseUrl(baseUrl)
...Then override at runtime:
mvn gatling:test -DbaseUrl=https://prod.api.example.com \
-Dgatling.simulationClass=com.example.ApiLoadTestNext Steps
A working simulation is just the starting point. From here:
- Add feeders to inject realistic test data (CSV files, JSON arrays, database queries) instead of hard-coded values.
- Use
.check()for chaining — save tokens, order IDs, and session cookies from earlier responses and replay them in later requests. - Integrate with CI — run Gatling in GitHub Actions or Jenkins, publish the HTML report as a build artifact, and fail the pipeline if p95 exceeds your SLO.
- Track regressions — save baseline numbers and compare percentiles across runs to catch performance regressions before they reach production.
Gatling rewards investment. The more realistic your simulation, the more useful the data it produces. Start simple, get a green run, then iterate toward production-fidelity load.