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 -versionYou 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:
- Go to the Gatling downloads page
- Download the latest open-source bundle (ZIP file)
- 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/binAdd 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 simultaneouslyrampUsers(n).during(duration)— ramp up linearly over a durationconstantUsersPerSec(rate).during(duration)— constant arrival rateheavisideUsers(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:
- Sends a GET request and checks the status is 200 and response time under 2 seconds
- Pauses randomly between 1-3 seconds (simulating user think time)
- Sends a POST request and validates the JSON response
- Runs with a progressive load injection
Running the Simulation
Run Gatling from the bundle directory:
cd $GATLING_HOME
bin/gatling.shYou'll see an interactive menu listing available simulations. Select yours by number. Alternatively, run non-interactively:
bin/gatling.sh -s BasicSimulationOr 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,pass3val 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 2sConditional 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 YourSimulationIntegrating 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:
- Maven/Gradle integration for managing Gatling as a project dependency
- Gatling Recorder — a proxy-based tool that records browser sessions and converts them to simulations automatically
- Distributed testing for generating load beyond a single machine's capacity
- 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.