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
- JMeter Tutorial - GUI-based load testing
- k6 Load Testing Guide - JavaScript-based performance testing
- API Testing Guide - REST API testing fundamentals
- CI/CD Testing Integration - Continuous testing pipelines
