What Is Gatling?
Gatling is a high-performance, open-source load testing tool designed for continuous integration and developer-friendly workflows. Built on Scala with an asynchronous, non-blocking architecture (Akka + Netty), Gatling can simulate thousands of concurrent users with significantly lower memory consumption than thread-based tools like JMeter.
Gatling produces detailed, interactive HTML reports out of the box — often considered the best-looking performance test reports in the industry. These reports include response time distributions, percentiles, request counts over time, and error analysis.
When to Choose Gatling
Each load testing tool has its strengths. Here is how Gatling compares.
| Feature | Gatling | JMeter | k6 |
|---|---|---|---|
| Language | Scala/Java/Kotlin DSL | GUI + XML | JavaScript |
| Architecture | Async non-blocking | Thread per user | Go goroutines |
| Reports | Beautiful HTML built-in | Basic, needs plugins | Terminal + JSON/CSV |
| CI/CD | Maven/Gradle/sbt plugin | CLI mode | CLI native |
| Protocol | HTTP, WebSocket, JMS, MQTT | HTTP, JDBC, FTP, LDAP, JMS | HTTP, WebSocket, gRPC |
| Resource usage | Very low | High | Low |
| Learning curve | Steep (Scala) | Moderate | Easy (JS) |
Choose Gatling when: You need excellent reporting, your team uses JVM languages, you want low resource usage at scale, or you need native Maven/Gradle integration for CI/CD.
Gatling Simulation Structure
A Gatling simulation is a Scala class that extends Simulation. It contains three main parts:
- Protocol configuration — HTTP settings, base URL, headers
- Scenario definition — The user journey (sequence of requests)
- Injection profile — How users are added over time
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class BasicSimulation extends Simulation {
// 1. Protocol configuration
val httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
// 2. Scenario definition
val scn = scenario("Browse Products")
.exec(
http("List Products")
.get("/api/products")
.check(status.is(200))
.check(jsonPath("$.products").exists)
)
.pause(2)
.exec(
http("Product Details")
.get("/api/products/1")
.check(status.is(200))
.check(jsonPath("$.name").saveAs("productName"))
)
.pause(1)
// 3. Setup with injection profile
setUp(
scn.inject(
rampUsers(100).during(60.seconds)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile3.lt(1000),
global.successfulRequests.percent.gt(99)
)
}
Protocol Configuration
The HTTP protocol builder defines shared settings for all requests:
val httpProtocol = http
.baseUrl("https://api.example.com") // base URL for all requests
.acceptHeader("application/json") // default Accept header
.acceptEncodingHeader("gzip, deflate") // compression
.userAgentHeader("Gatling/3.9") // user agent
.shareConnections // connection pooling
Scenario Definition
A scenario is a sequence of actions that a virtual user performs:
val scn = scenario("User Journey")
.exec(http("Homepage").get("/"))
.pause(1, 3) // random pause between 1-3 seconds
.exec(http("Login").post("/login")
.body(StringBody("""{"user":"test","pass":"123"}"""))
.check(jsonPath("$.token").saveAs("authToken"))
)
.exec(http("Dashboard").get("/dashboard")
.header("Authorization", "Bearer ${authToken}")
)
Key actions:
exec()— Execute an HTTP requestpause()— Wait between requests (think time)repeat()— Loop a sequence N timesduring()— Loop for a specified durationdoIf()/doIfOrElse()— Conditional executionfeed()— Inject data from a feeder (CSV, JSON, JDBC)
Feeders (Data Parameterization)
Feeders supply test data to virtual users:
// CSV feeder
val csvFeeder = csv("users.csv").circular // recycle when exhausted
// JSON feeder
val jsonFeeder = jsonFile("products.json").random
// Inline feeder
val inlineFeeder = Iterator.continually(Map(
"username" -> s"user_${scala.util.Random.nextInt(1000)}",
"email" -> s"user${scala.util.Random.nextInt(1000)}@test.com"
))
val scn = scenario("Parameterized Test")
.feed(csvFeeder)
.exec(http("Login")
.post("/login")
.body(StringBody("""{"username":"${username}","password":"${password}"}"""))
)
Checks and Assertions
Checks validate individual responses:
http("Get User")
.get("/api/users/1")
.check(status.is(200)) // status code
.check(jsonPath("$.name").is("John")) // JSON value
.check(responseTimeInMillis.lt(500)) // response time
.check(jsonPath("$.id").saveAs("userId")) // save for later use
.check(header("Content-Type").is("application/json"))
Assertions define pass/fail criteria for the entire simulation:
setUp(scn.inject(...))
.assertions(
global.responseTime.max.lt(5000), // max response time < 5s
global.responseTime.percentile3.lt(1000), // p75 < 1s
global.successfulRequests.percent.gt(99), // > 99% success rate
details("Login").responseTime.mean.lt(500) // Login avg < 500ms
)
Injection Profiles
Injection profiles define how virtual users are introduced into the simulation. Gatling offers several strategies:
setUp(
scn.inject(
// Open model (users arrive at a rate)
nothingFor(5.seconds), // wait 5 seconds
atOnceUsers(10), // inject 10 users immediately
rampUsers(100).during(60.seconds), // ramp to 100 users over 60s
constantUsersPerSec(20).during(120.seconds), // 20 users/sec for 2 min
rampUsersPerSec(1).to(50).during(60.seconds) // ramp from 1 to 50 users/sec
)
)
These profiles can be combined to create complex load patterns.
Running Gatling
# Using the bundle
./bin/gatling.sh
# Using Maven
mvn gatling:test
# Using Gradle
gradle gatlingRun
# Using sbt
sbt Gatling/test
Gatling generates an HTML report in target/gatling/ with charts for response times, throughput, and active users over time.
Exercise: Design a Gatling Simulation
Create a Gatling simulation for an online auction platform.
Scenario
The auction platform has these endpoints:
| Endpoint | Method | Description |
|---|---|---|
/api/auth/login | POST | User login |
/api/auctions | GET | List active auctions |
/api/auctions/{id} | GET | Auction details |
/api/auctions/{id}/bid | POST | Place a bid |
Requirements
- Use a CSV feeder for user credentials
- Chain requests: login, browse auctions, view details, place bid
- Extract the auth token from login and auction ID from the list
- Add pauses between actions (1-3 seconds)
- Injection profile: ramp 50 users over 2 minutes, hold for 3 minutes, ramp down
- Assertions: p95 < 1000ms, success rate > 98%
Hint: Simulation Structure
class AuctionSimulation extends Simulation {
val httpProtocol = http.baseUrl("https://auction-api.example.com")
val csvFeeder = csv("users.csv").circular
val scn = scenario("Auction Bidding")
.feed(csvFeeder)
.exec(/* login */)
.pause(1, 3)
.exec(/* list auctions, save auctionId */)
.pause(1, 2)
.exec(/* view auction details */)
.pause(1, 2)
.exec(/* place bid */)
setUp(
scn.inject(
rampUsers(50).during(120.seconds),
nothingFor(180.seconds),
// consider using constantUsersPerSec for steady state
)
).protocols(httpProtocol)
.assertions(/* your assertions */)
}
Solution: Complete Gatling Simulation
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class AuctionSimulation extends Simulation {
val httpProtocol = http
.baseUrl("https://auction-api.example.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
val csvFeeder = csv("users.csv").circular
val scn = scenario("Auction Bidding Flow")
.feed(csvFeeder)
// Step 1: Login
.exec(
http("Login")
.post("/api/auth/login")
.body(StringBody("""{"username":"${username}","password":"${password}"}"""))
.check(status.is(200))
.check(jsonPath("$.token").saveAs("authToken"))
)
.pause(1, 3)
// Step 2: List auctions
.exec(
http("List Auctions")
.get("/api/auctions")
.header("Authorization", "Bearer ${authToken}")
.check(status.is(200))
.check(jsonPath("$.auctions[0].id").saveAs("auctionId"))
.check(jsonPath("$.auctions[0].currentBid").saveAs("currentBid"))
)
.pause(1, 2)
// Step 3: View auction details
.exec(
http("Auction Details")
.get("/api/auctions/${auctionId}")
.header("Authorization", "Bearer ${authToken}")
.check(status.is(200))
.check(jsonPath("$.title").exists)
)
.pause(1, 2)
// Step 4: Place bid
.exec(session => {
val currentBid = session("currentBid").as[String].toDouble
val newBid = currentBid + 10.0
session.set("newBid", newBid.toString)
})
.exec(
http("Place Bid")
.post("/api/auctions/${auctionId}/bid")
.header("Authorization", "Bearer ${authToken}")
.body(StringBody("""{"amount":${newBid}}"""))
.check(status.in(200, 201))
)
setUp(
scn.inject(
rampUsers(50).during(120.seconds),
nothingFor(180.seconds)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile(95).lt(1000),
global.successfulRequests.percent.gt(98)
)
}
users.csv:
username,password
bidder1,pass123
bidder2,pass456
bidder3,pass789
Analyzing the HTML Report:
- Response Time Distribution: Check if the histogram skews left (fast responses)
- Active Users Over Time: Verify the ramp-up pattern matches your injection profile
- Requests per Second: Look for throughput stability during steady state
- Response Time Percentiles: p95 should remain under 1000ms throughout the test
- Errors tab: Identify which requests fail and at what user count
Pro Tips
- Java/Kotlin DSL: If your team does not know Scala, Gatling 3.7+ supports Java and Kotlin DSLs that are equally powerful. The Java DSL is the recommended starting point for most teams.
- Closed vs Open Model: Gatling supports both closed-model (fixed number of concurrent users) and open-model (users arrive at a rate). Use open model for web applications where new users constantly arrive regardless of how many are already active.
- Gatling Recorder: Similar to JMeter’s recorder, Gatling includes a HAR converter and HTTP proxy recorder for capturing browser traffic and converting it to Gatling scripts.
- Maven Archetype: Start new projects with
mvn archetype:generate -DarchetypeGroupId=io.gatling.highcharts -DarchetypeArtifactId=gatling-highcharts-maven-archetypefor a pre-configured project structure. - CI Integration: Gatling’s Maven/Gradle plugins make it straightforward to run load tests in Jenkins, GitHub Actions, or GitLab CI. Set assertions to fail the build if performance degrades.