TL;DR

  • Gatling uses Scala DSL for readable, maintainable load test scripts
  • Async architecture handles thousands of virtual users with low resource usage
  • Feeders inject test data, assertions validate performance requirements
  • Beautiful HTML reports generated automatically after each run
  • Code-based approach integrates naturally with CI/CD pipelines

Best for: Teams wanting maintainable load tests as code, high concurrency scenarios Skip if: No coding experience (JMeter GUI might be easier to start) Reading time: 15 minutes

Your JMeter tests work but the scripts are becoming unmaintainable. The XML files are impossible to review in pull requests. Running 10,000 virtual users requires multiple machines.

Gatling solves this. Tests are written in Scala DSL — readable code that lives in your repository. The async architecture simulates thousands of users on a single machine. Reports are generated automatically with detailed metrics.

This tutorial covers Gatling from installation to CI/CD integration — everything for high-performance load testing.

What is Gatling?

Gatling is an open-source load testing framework built on Scala, Akka, and Netty. It uses an asynchronous, non-blocking architecture that efficiently simulates massive concurrent user loads.

Why Gatling:

  • High performance — async model handles 10,000+ users per machine
  • Code as tests — Scala DSL integrates with version control and CI/CD
  • Beautiful reports — detailed HTML reports with charts and percentiles
  • Developer-friendly — IDE support, debugging, code reuse
  • Resource efficient — lower CPU/memory than thread-based tools

Installation

Prerequisites

# Java 11+ required
java -version

# Download Gatling
# Option 1: Download from https://gatling.io/open-source/
# Option 2: Maven/Gradle dependency

Maven Setup

<dependencies>
    <dependency>
        <groupId>io.gatling.highcharts</groupId>
        <artifactId>gatling-charts-highcharts</artifactId>
        <version>3.10.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>io.gatling</groupId>
            <artifactId>gatling-maven-plugin</artifactId>
            <version>4.7.0</version>
        </plugin>
    </plugins>
</build>

Project Structure

project/
├── src/
│   └── test/
│       ├── scala/
│       │   └── simulations/
│       │       └── BasicSimulation.scala
│       └── resources/
│           ├── gatling.conf
│           ├── data/
│           │   └── users.csv
│           └── bodies/
│               └── request.json
├── pom.xml
└── target/
    └── gatling/
        └── results/

First Simulation

Basic Structure

package simulations

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

class BasicSimulation extends Simulation {

  // HTTP Configuration
  val httpProtocol = http
    .baseUrl("https://api.example.com")
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")

  // Scenario Definition
  val scn = scenario("Basic API Test")
    .exec(
      http("Get Users")
        .get("/users")
        .check(status.is(200))
    )
    .pause(1.second)
    .exec(
      http("Get User Details")
        .get("/users/1")
        .check(status.is(200))
        .check(jsonPath("$.name").exists)
    )

  // Load Profile
  setUp(
    scn.inject(atOnceUsers(10))
  ).protocols(httpProtocol)
}

Running Tests

# Run all simulations
mvn gatling:test

# Run specific simulation
mvn gatling:test -Dgatling.simulationClass=simulations.BasicSimulation

# With Gatling bundle
./bin/gatling.sh

HTTP Requests

Request Types

class HttpExamplesSimulation extends Simulation {

  val httpProtocol = http.baseUrl("https://api.example.com")

  val scn = scenario("HTTP Examples")
    // GET request
    .exec(
      http("Get Request")
        .get("/users")
        .queryParam("page", "1")
        .queryParam("limit", "10")
    )
    // POST with JSON body
    .exec(
      http("Create User")
        .post("/users")
        .body(StringBody("""{"name":"John","email":"john@example.com"}"""))
        .asJson
    )
    // POST with file body
    .exec(
      http("Create from File")
        .post("/users")
        .body(RawFileBody("bodies/user.json"))
        .asJson
    )
    // PUT request
    .exec(
      http("Update User")
        .put("/users/1")
        .body(StringBody("""{"name":"Updated Name"}"""))
        .asJson
    )
    // DELETE request
    .exec(
      http("Delete User")
        .delete("/users/1")
    )

  setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}

Headers and Authentication

val httpProtocol = http
  .baseUrl("https://api.example.com")
  .acceptHeader("application/json")
  .contentTypeHeader("application/json")
  .authorizationHeader("Bearer ${token}")
  .userAgentHeader("Gatling/3.10")

// Or per request
.exec(
  http("Authenticated Request")
    .get("/protected")
    .header("Authorization", "Bearer ${token}")
    .header("X-Custom-Header", "value")
)

Session and Variables

Saving Response Data

val scn = scenario("Session Example")
  // Save value from response
  .exec(
    http("Login")
      .post("/auth/login")
      .body(StringBody("""{"email":"user@example.com","password":"pass123"}"""))
      .asJson
      .check(jsonPath("$.token").saveAs("authToken"))
      .check(jsonPath("$.user.id").saveAs("userId"))
  )
  // Use saved values
  .exec(
    http("Get Profile")
      .get("/users/${userId}")
      .header("Authorization", "Bearer ${authToken}")
  )
  // Debug: print session
  .exec { session =>
    println(s"Token: ${session("authToken").as[String]}")
    println(s"User ID: ${session("userId").as[String]}")
    session
  }

Session Functions

.exec { session =>
  // Modify session
  session.set("customVar", "value")
}

.exec { session =>
  // Conditional logic
  val userId = session("userId").as[String]
  if (userId.toInt > 100) {
    session.set("userType", "premium")
  } else {
    session.set("userType", "standard")
  }
}

Feeders (Test Data)

CSV Feeder

# data/users.csv
username,password,role
john@example.com,pass123,admin
jane@example.com,pass456,user
bob@example.com,pass789,user
val csvFeeder = csv("data/users.csv").random

val scn = scenario("Login Test")
  .feed(csvFeeder)
  .exec(
    http("Login")
      .post("/auth/login")
      .body(StringBody("""{"email":"${username}","password":"${password}"}"""))
      .asJson
  )

Feeder Strategies

// Sequential - each user gets next row
val seqFeeder = csv("data/users.csv").queue

// Circular - loops back to start
val circularFeeder = csv("data/users.csv").circular

// Random - random row each time
val randomFeeder = csv("data/users.csv").random

// Shuffle - random but each row used once
val shuffleFeeder = csv("data/users.csv").shuffle

// JSON feeder
val jsonFeeder = jsonFile("data/users.json").circular

// Custom feeder
val customFeeder = Iterator.continually(Map(
  "email" -> s"user${scala.util.Random.nextInt(1000)}@example.com",
  "timestamp" -> System.currentTimeMillis()
))

Checks and Assertions

Response Checks

.exec(
  http("Get Users")
    .get("/users")
    // Status check
    .check(status.is(200))
    // Response time check
    .check(responseTimeInMillis.lt(2000))
    // Header check
    .check(header("Content-Type").is("application/json"))
    // JSON checks
    .check(jsonPath("$").exists)
    .check(jsonPath("$.data").ofType[Seq[Any]])
    .check(jsonPath("$.data[*].id").findAll.saveAs("userIds"))
    .check(jsonPath("$.meta.total").ofType[Int].gt(0))
    // Body string check
    .check(bodyString.exists)
    .check(substring("success").exists)
)

Global Assertions

setUp(
  scn.inject(rampUsers(100).during(60.seconds))
).protocols(httpProtocol)
  .assertions(
    // Response time assertions
    global.responseTime.max.lt(5000),
    global.responseTime.percentile(95).lt(2000),
    global.responseTime.mean.lt(1000),
    // Success rate
    global.successfulRequests.percent.gt(99),
    // Request per second
    global.requestsPerSec.gt(100),
    // Specific request
    details("Login").responseTime.max.lt(3000),
    details("Login").failedRequests.percent.lt(1)
  )

Load Profiles

Injection Patterns

setUp(
  // Fixed users at once
  scn.inject(atOnceUsers(100)),

  // Ramp up over time
  scn.inject(rampUsers(100).during(60.seconds)),

  // Constant rate
  scn.inject(constantUsersPerSec(10).during(5.minutes)),

  // Ramp rate
  scn.inject(
    rampUsersPerSec(1).to(100).during(2.minutes)
  ),

  // Stepped load
  scn.inject(
    incrementUsersPerSec(10)
      .times(5)
      .eachLevelLasting(30.seconds)
      .separatedByRampsLasting(10.seconds)
      .startingFrom(10)
  ),

  // Complex profile
  scn.inject(
    nothingFor(5.seconds),
    atOnceUsers(10),
    rampUsers(50).during(30.seconds),
    constantUsersPerSec(20).during(2.minutes),
    rampUsersPerSec(20).to(50).during(1.minute)
  )
)

Multiple Scenarios

val loginScn = scenario("Login Flow")
  .exec(/* login steps */)

val browseScn = scenario("Browse Products")
  .exec(/* browse steps */)

val checkoutScn = scenario("Checkout")
  .exec(/* checkout steps */)

setUp(
  loginScn.inject(rampUsers(100).during(1.minute)),
  browseScn.inject(rampUsers(200).during(1.minute)),
  checkoutScn.inject(rampUsers(50).during(1.minute))
).protocols(httpProtocol)

Scenario Flow Control

Pauses

val scn = scenario("With Pauses")
  .exec(http("Request 1").get("/api/1"))
  .pause(1.second)                     // Fixed pause
  .exec(http("Request 2").get("/api/2"))
  .pause(1.second, 3.seconds)          // Random between 1-3s
  .exec(http("Request 3").get("/api/3"))
  .pause(normalPausesWithStdDevDuration(2.seconds, 500.millis)) // Normal distribution

Loops and Conditions

val scn = scenario("Flow Control")
  // Repeat fixed times
  .repeat(5) {
    exec(http("Repeated").get("/api/data"))
  }
  // Repeat with counter
  .repeat(3, "counter") {
    exec(http("Item ${counter}").get("/api/items/${counter}"))
  }
  // Loop during time
  .during(30.seconds) {
    exec(http("Looped").get("/api/status"))
      .pause(1.second)
  }
  // Conditional execution
  .doIf("${userType.equals('premium')}") {
    exec(http("Premium Feature").get("/api/premium"))
  }
  // Random switch
  .randomSwitch(
    60.0 -> exec(http("Path A").get("/api/a")),
    40.0 -> exec(http("Path B").get("/api/b"))
  )

Error Handling

val scn = scenario("Error Handling")
  .exec(
    http("Might Fail")
      .get("/api/unreliable")
      .check(status.is(200))
  )
  .exitHereIfFailed  // Stop user if previous failed
  .exec(http("After Success").get("/api/next"))

// Try-catch style
.tryMax(3) {
  exec(http("Retry Request").get("/api/flaky"))
}

// Exit block on failure
.exitBlockOnFail {
  exec(http("Critical").get("/api/critical"))
  exec(http("Dependent").get("/api/dependent"))
}

Reports

HTML Report

Gatling generates detailed HTML reports automatically:

target/gatling/basicsimulation-20260128120000/
├── index.html              # Main report
├── js/
├── style/
└── simulation.log

Report sections:

  • Global Information — total requests, success/failure rates
  • Response Time Distribution — histogram of response times
  • Response Time Percentiles — 50th, 75th, 95th, 99th percentiles over time
  • Requests per Second — throughput graph
  • Responses per Second — server response rate
  • Active Users — concurrent users during test

Console Output

// Enable console summary during test
val httpProtocol = http
  .baseUrl("https://api.example.com")
  .shareConnections

// In gatling.conf
gatling {
  charting {
    indicators {
      lowerBound = 800
      higherBound = 1200
    }
  }
}

CI/CD Integration

GitHub Actions

name: Load Tests

on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM
  workflow_dispatch:

jobs:
  gatling:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Run Gatling tests
        run: mvn gatling:test -Dgatling.simulationClass=simulations.LoadTest

      - name: Upload report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: gatling-report
          path: target/gatling/*/

      - name: Fail on assertion errors
        run: |
          if grep -q "KO" target/gatling/*/simulation.log; then
            echo "Performance assertions failed!"
            exit 1
          fi

Jenkins Pipeline

pipeline {
    agent any

    stages {
        stage('Load Test') {
            steps {
                sh 'mvn gatling:test'
            }
        }
    }

    post {
        always {
            gatlingArchive()
        }
    }
}

Gatling with AI Assistance

AI tools can help write and optimize Gatling simulations.

What AI does well:

  • Generate scenarios from API documentation
  • Create realistic feeder data
  • Suggest appropriate load profiles
  • Convert other formats to Gatling DSL

What still needs humans:

  • Defining realistic user behavior patterns
  • Setting meaningful performance thresholds
  • Analyzing results in business context
  • Capacity planning decisions

FAQ

What is Gatling?

Gatling is a Scala-based open-source load testing tool designed for high performance and developer-friendly workflows. It uses an asynchronous, non-blocking architecture built on Akka and Netty that efficiently simulates thousands of concurrent users with low resource consumption. Tests are written as code using a readable Scala DSL.

Is Gatling free?

Yes, Gatling Open Source is completely free under the Apache 2.0 license. It includes the full testing engine, Scala DSL, and HTML reporting. Gatling Enterprise (formerly Gatling FrontLine) is a paid product that adds distributed testing, real-time monitoring, advanced analytics, and team collaboration features for organizations needing additional scale and management capabilities.

Gatling vs JMeter — which is better?

Gatling excels at high concurrency scenarios with lower resource usage due to its async architecture — one Gatling instance can simulate as many users as 3-4 JMeter instances. Gatling scripts are code, making them maintainable and CI/CD friendly. JMeter has a GUI that’s easier for beginners and a larger ecosystem of plugins. Choose Gatling for developer-led performance testing; choose JMeter for teams preferring visual test creation.

Do I need to know Scala for Gatling?

Basic Scala knowledge helps but isn’t required to be productive. Gatling’s DSL is designed to be readable and most scenarios use simple method chains like .get(), .check(), .saveAs(). You can write effective load tests within hours of starting. For complex scenarios with custom logic, Scala knowledge becomes more useful for session manipulation and conditional flows.

Official Resources

See Also