Gatling API Load Testing: A Quickstart Guide for Developers

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, and atOnceUsers cover 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=false

This 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.xml

Open 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 minute

Use 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.ApiLoadTest

With Gradle:

./gradlew gatlingRun-com.example.ApiLoadTest

Gatling 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.ApiLoadTest

Next 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.

Read more

Start now free