Gatling Load Testing: A Complete Getting Started Guide

Gatling Load Testing: A Complete Getting Started Guide

Performance testing is no longer optional. As applications scale and user expectations rise, knowing how your system behaves under load is fundamental to shipping reliable software. Gatling is one of the most powerful open-source load testing tools available—built on Scala and Akka, it handles massive concurrency with minimal resource overhead. This guide walks you through everything you need to get Gatling running from scratch.

What Is Gatling?

Gatling is an open-source load testing tool designed for web applications, particularly HTTP-based APIs and services. Unlike tools that rely on threads for concurrency, Gatling uses an asynchronous, non-blocking architecture built on Akka actors. This means a single Gatling instance can simulate thousands of concurrent virtual users without the memory overhead of thread-per-user approaches.

Key characteristics:

  • Scala-based DSL: Simulations are written in Scala, giving you the full power of a programming language
  • Detailed HTML reports: Automatically generated reports with percentile breakdowns, response time distributions, and request-level statistics
  • High performance: Can generate significant load from a single machine
  • CI/CD friendly: Works well with Maven, Gradle, and GitHub Actions

Gatling is particularly popular for API testing and microservices load testing, where you need precise control over request patterns.

Installation

Prerequisites

Gatling requires Java 11 or later. Verify your Java installation:

java -version

You should see output like openjdk version "17.0.x" or similar. If Java isn't installed, download it from adoptium.net.

Download the Bundle

The easiest way to get started is with the Gatling bundle:

  1. Go to the Gatling downloads page
  2. Download the latest open-source bundle (ZIP file)
  3. Extract to a directory of your choice, e.g., /opt/gatling

The bundle structure:

gatling-charts-highcharts-bundle-3.x.x/
├── bin/
│   ├── gatling.sh      # Unix launcher
│   └── gatling.bat     # Windows launcher
├── conf/               # Configuration files
├── results/            # Test results (generated)
├── simulations/        # Your simulation files go here
└── user-files/         # Resources (data files, bodies)

Set Environment Variable

For convenience, set GATLING_HOME:

export GATLING_HOME=/opt/gatling
export PATH=$PATH:$GATLING_HOME/bin

Add these to your .bashrc or .zshrc for persistence.

Understanding Gatling Concepts

Before writing your first simulation, understand the core building blocks:

Simulation

A Simulation is the top-level class. It extends Simulation and defines scenarios, protocol configurations, and load injection patterns.

class MyFirstSimulation extends Simulation {
  // scenarios, protocols, and setUp go here
}

Scenario

A scenario defines a sequence of actions a virtual user performs. Think of it as a user journey—login, browse, add to cart, checkout.

val scn = scenario("Basic User Journey")
  .exec(http("Home Page").get("/"))
  .pause(1)
  .exec(http("Products").get("/products"))

Protocol

The protocol configuration defines how Gatling communicates with your target system. For HTTP testing:

val httpProtocol = http
  .baseUrl("https://api.example.com")
  .acceptHeader("application/json")
  .userAgentHeader("Gatling/3.x")

Injection

The injection profile controls how virtual users are added to the simulation:

setUp(
  scn.inject(atOnceUsers(100))
).protocols(httpProtocol)

Common injection patterns:

  • atOnceUsers(n) — inject n users simultaneously
  • rampUsers(n).during(duration) — ramp up linearly over a duration
  • constantUsersPerSec(rate).during(duration) — constant arrival rate
  • heavisideUsers(n).during(duration) — smooth ramp shaped like a Heaviside function

Writing Your First Simulation

Create a new file in simulations/ called BasicSimulation.scala:

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BasicSimulation extends Simulation {

  // Define the HTTP protocol
  val httpProtocol = http
    .baseUrl("https://httpbin.org")
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")

  // Define the scenario
  val scn = scenario("Basic HTTP Test")
    .exec(
      http("GET Request")
        .get("/get")
        .check(status.is(200))
        .check(responseTimeInMillis.lte(2000))
    )
    .pause(1, 3) // pause between 1 and 3 seconds
    .exec(
      http("POST Request")
        .post("/post")
        .body(StringBody("""{"user": "gatling", "action": "test"}""")).asJson
        .check(status.is(200))
        .check(jsonPath("$.json.user").is("gatling"))
    )

  // Define the load profile
  setUp(
    scn.inject(
      atOnceUsers(1),          // Start with 1 user
      rampUsers(10).during(10.seconds), // Ramp to 10 users over 10 seconds
      constantUsersPerSec(5).during(30.seconds) // Hold at 5 users/sec for 30 seconds
    )
  ).protocols(httpProtocol)
}

This simulation:

  1. Sends a GET request and checks the status is 200 and response time under 2 seconds
  2. Pauses randomly between 1-3 seconds (simulating user think time)
  3. Sends a POST request and validates the JSON response
  4. Runs with a progressive load injection

Running the Simulation

Run Gatling from the bundle directory:

cd $GATLING_HOME
bin/gatling.sh

You'll see an interactive menu listing available simulations. Select yours by number. Alternatively, run non-interactively:

bin/gatling.sh -s BasicSimulation

Or with a description:

bin/gatling.sh -s BasicSimulation -rd "Load test run #1"

Console Output

During the run, Gatling shows live statistics:

================================================================================
2026-01-15 14:23:45                                           5s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=45   KO=0     )
> GET Request                                              (OK=23   KO=0     )
> POST Request                                             (OK=22   KO=0     )
---- Basic HTTP Test -----------------------------------------------------------
[--------------------------------------------------------------------------]  0%
          waiting: 85     / active: 15     / done: 0
================================================================================

Understanding the Reports

After the simulation completes, Gatling generates an HTML report in results/. Open index.html in a browser.

Key Metrics to Review

Response Time Distribution The histogram shows how response times are distributed. A healthy distribution clusters near the mean. Bimodal distributions often indicate intermittent performance issues.

Active Users Over Time Visualizes your injection profile. Verify this matches your intended load pattern.

Response Time Percentiles The most important table in the report:

Metric Description
min Fastest response
50th percentile Median—50% of requests faster than this
75th percentile 75% of requests faster than this
95th percentile Critical SLO metric—most users' experience
99th percentile Catches tail latency issues
max Slowest response—often an outlier

Focus on the 95th and 99th percentiles. If your SLO is "99% of requests under 500ms," the report tells you directly whether you're meeting it.

Requests Per Second Shows throughput over time. A sudden drop often correlates with server saturation.

Errors Any non-2xx responses or connection failures appear here. Zero errors is the target; investigate anything above 0.1%.

Adding Checks and Assertions

Checks validate individual request responses. Assertions validate the overall test results against thresholds.

Checks

.exec(
  http("API Call")
    .get("/api/users")
    .check(status.is(200))
    .check(header("Content-Type").is("application/json"))
    .check(jsonPath("$.users").exists)
    .check(jsonPath("$.users[0].id").saveAs("userId"))
    .check(bodyLength.gt(0))
    .check(responseTimeInMillis.lte(500))
)

You can save values from responses using saveAs and use them in subsequent requests:

.exec(
  http("Get User")
    .get("/api/users/#{userId}")  // uses saved userId
    .check(status.is(200))
)

Assertions

Assertions fail the simulation if thresholds aren't met:

setUp(
  scn.inject(rampUsers(100).during(1.minute))
).protocols(httpProtocol)
 .assertions(
   global.responseTime.percentile(95).lt(500),     // p95 < 500ms
   global.successfulRequests.percent.gt(99),        // 99%+ success rate
   forAll.responseTime.mean.lt(300)                 // All request types avg < 300ms
 )

When assertions fail, Gatling exits with a non-zero status code—useful for failing CI/CD pipelines.

Feeding Test Data

Real load tests require varied data. Gatling's feeders inject data into virtual user sessions.

CSV Feeder

# users.csv
username,password
user1,pass1
user2,pass2
user3,pass3
val feeder = csv("users.csv").circular

val scn = scenario("Login Test")
  .feed(feeder)
  .exec(
    http("Login")
      .post("/api/login")
      .body(StringBody("""{"username": "#{username}", "password": "#{password}"}""")).asJson
      .check(status.is(200))
  )

Random Data Feeder

import java.util.UUID

val feeder = Iterator.continually(Map(
  "orderId" -> UUID.randomUUID().toString,
  "amount" -> (scala.util.Random.nextDouble() * 1000).round
))

val scn = scenario("Order Creation")
  .feed(feeder)
  .exec(
    http("Create Order")
      .post("/api/orders")
      .body(StringBody("""{"id": "#{orderId}", "amount": #{amount}}""")).asJson
      .check(status.is(201))
  )

Common Patterns for Real-World Tests

Authentication Flow

Most APIs require authentication. Set up auth once per virtual user, then reuse the token:

val scn = scenario("Authenticated API Test")
  .exec(
    http("Login")
      .post("/api/auth/login")
      .body(StringBody("""{"email": "test@example.com", "password": "secret"}""")).asJson
      .check(status.is(200))
      .check(jsonPath("$.token").saveAs("authToken"))
  )
  .exec(
    http("Get Profile")
      .get("/api/profile")
      .header("Authorization", "Bearer #{authToken}")
      .check(status.is(200))
  )

Think Time

Real users don't hammer APIs continuously. Add think time to simulate realistic behavior:

.pause(500.milliseconds, 2.seconds)  // Random pause between 0.5s and 2s

Conditional Logic

.exec(session => {
  val userType = if (session("isAdmin").as[Boolean]) "admin" else "regular"
  session.set("userType", userType)
})
.doIf(session => session("userType").as[String] == "admin")(
  exec(http("Admin Dashboard").get("/admin"))
)

Configuration Tuning

For high-load tests, tune conf/gatling.conf:

gatling {
  core {
    outputDirectoryBaseName = "results"
    runDescription = "Load test"
  }
  http {
    maxConnectionsPerHost = 6
    perUserKeyManagerFactory = null
    dns {
      queryTimeout = 5 seconds
      maxQueriesPerResolve = 6
    }
  }
  data {
    writers = [console, file, graphite]
  }
}

Increase JVM heap for large simulations:

JAVA_OPTS="-Xms512m -Xmx4g" bin/gatling.sh -s YourSimulation

Integrating with Your Testing Pipeline

Gatling pairs naturally with automated testing pipelines. A simple approach using the bundle:

# .github/workflows/load-test.yml
name: Load Tests
on:
  workflow_dispatch:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2am

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '17'
      - name: Download Gatling
        run: |
          wget https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/3.10.5/gatling-charts-highcharts-bundle-3.10.5-bundle.zip
          unzip gatling-charts-highcharts-bundle-3.10.5-bundle.zip
      - name: Run simulation
        run: |
          gatling-charts-highcharts-bundle-3.10.5/bin/gatling.sh -s BasicSimulation
      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: gatling-results
          path: gatling-charts-highcharts-bundle-3.10.5/results/

What to Test and What Not to

Good Candidates for Gatling

  • REST APIs under sustained load
  • Authentication endpoints (rate limiting, session handling)
  • Read-heavy endpoints (product listings, search)
  • Write operations (order creation, form submissions)
  • Long-running operations (report generation, data exports)

Consider These Limitations

  • Gatling isn't a browser simulator—it doesn't execute JavaScript. For browser-level performance testing, consider k6 with browser module or Playwright
  • WebSocket support exists but requires different configuration
  • Very complex JavaScript-heavy SPAs may need additional tooling for realistic user simulation

Next Steps

You now have a working Gatling setup and understand the fundamental concepts. From here, explore:

  1. Maven/Gradle integration for managing Gatling as a project dependency
  2. Gatling Recorder — a proxy-based tool that records browser sessions and converts them to simulations automatically
  3. Distributed testing for generating load beyond a single machine's capacity
  4. Gatling Enterprise (commercial) for cloud-based distributed testing with enhanced reporting

Load testing early and often prevents the unpleasant surprise of your application melting down under real traffic. A simulation that takes 20 minutes to write can prevent hours of incident response.

Read more

Start now free