Gatling is a powerful, high-performance load testing tool designed for testing web applications, APIs, and microservices (as discussed in K6: Modern Load Testing with JavaScript for DevOps Teams). Built on Akka and Netty, Gatling (as discussed in Load Testing with JMeter: Complete Guide) excels at simulating thousands of concurrent users with minimal resource consumption while providing detailed, actionable performance metrics through beautiful HTML reports.
Why Gatling for Performance Testing?
Gatling stands out in the performance testing landscape through its unique combination of developer-friendly DSL, exceptional performance, and enterprise-grade features:
- Scala DSL: Expressive, type-safe scripting with IDE support
- High performance: Async non-blocking architecture handles massive concurrency
- Realistic simulations: Advanced user modeling with think times and pacing
- Rich protocols: HTTP, WebSocket, SSE, JMS support out of the box
- Beautiful reports: Detailed HTML reports with charts and metrics
- CI/CD ready: Command-line execution and Maven/Gradle integration
Gatling vs Other Load Testing Tools
Feature | Gatling | JMeter | K6 | Locust |
---|---|---|---|---|
Scripting | Scala DSL | GUI/XML | JavaScript | Python |
Performance | Excellent | Medium | Excellent | Medium |
Resource Usage | Low | High | Low | Medium |
Protocol Support | HTTP, WS, JMS | Extensive | HTTP/2, WS, gRPC | HTTP |
Reporting | Excellent | Basic | Good | Basic |
Recorder | Yes | Yes | No | No |
CI/CD Integration | Native | Plugins | Native | Manual |
Getting Started with Gatling
Installation with Maven
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>gatling-tests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<gatling.version>3.10.3</gatling.version>
<gatling-maven-plugin.version>4.7.0</gatling-maven-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling-maven-plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
Installation with Gradle
plugins {
id 'scala'
id 'io.gatling.gradle' version '3.10.3.1'
}
repositories {
mavenCentral()
}
dependencies {
gatling 'io.gatling.highcharts:gatling-charts-highcharts:3.10.3'
}
gatling {
simulations = {
include '**/*Simulation.scala'
}
}
First Gatling Simulation
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class BasicSimulation extends Simulation {
// HTTP protocol configuration
val httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json")
.userAgentHeader("Gatling Performance Test")
// Scenario definition
val scn = scenario("Basic Load Test")
.exec(
http("Get Users")
.get("/users")
.check(status.is(200))
)
.pause(1)
.exec(
http("Get User Details")
.get("/users/1")
.check(status.is(200))
.check(jsonPath("$.name").exists)
)
// Load injection
setUp(
scn.inject(
atOnceUsers(10), // 10 users immediately
rampUsers(50) during (30.seconds) // 50 users over 30 seconds
)
).protocols(httpProtocol)
}
Run the simulation:
mvn gatling:test
# or
./gradlew gatlingRun
Load Injection Profiles
Open vs Closed Workload Models
// Open Model: New users arrive regardless of system performance
setUp(
scn.inject(
nothingFor(4.seconds), // Wait 4 seconds
atOnceUsers(10), // Inject 10 users at once
rampUsers(50) during (30.seconds), // Ramp up to 50 users
constantUsersPerSec(20) during (1.minute), // Constant rate
rampUsersPerSec(10) to 50 during (2.minutes) // Ramp rate
)
)
// Closed Model: Fixed number of concurrent users
setUp(
scn.inject(
constantConcurrentUsers(10) during (30.seconds),
rampConcurrentUsers(10) to 50 during (1.minute)
)
)
Advanced Injection Patterns
setUp(
// Stress test pattern
scn.inject(
rampUsers(10) during (1.minute), // Warm-up
constantUsersPerSec(100) during (5.minutes), // Sustained load
rampUsersPerSec(100) to 500 during (2.minutes), // Ramp to peak
constantUsersPerSec(500) during (5.minutes), // Peak load
rampUsersPerSec(500) to 100 during (2.minutes) // Cool down
),
// Spike test
scn.inject(
constantUsersPerSec(50) during (2.minutes),
atOnceUsers(500), // Sudden spike
constantUsersPerSec(50) during (2.minutes)
)
).protocols(httpProtocol)
Throttling
setUp(
scn.inject(rampUsers(1000) during (1.minute))
).protocols(httpProtocol)
.throttle(
reachRps(100) in (10.seconds), // Reach 100 RPS
holdFor(1.minute), // Maintain
jumpToRps(50), // Drop to 50
holdFor(2.minutes)
)
HTTP Protocol Configuration
Complete HTTP Protocol Setup
val httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json, text/html, */*")
.acceptEncodingHeader("gzip, deflate")
.acceptLanguageHeader("en-US,en;q=0.5")
.userAgentHeader("Gatling/3.10")
.shareConnections // Connection pooling
.disableWarmUp // Skip warmup requests
// Headers
.header("X-API-Version", "v1")
// Authentication
.authorizationHeader("Bearer ${token}")
// Proxy
.proxy(Proxy("proxy.example.com", 8080)
.credentials("username", "password"))
// Timeouts
.requestTimeout(30.seconds)
.readTimeout(30.seconds)
// Keep-alive
.connectionHeader("keep-alive")
// Automatic redirect handling
.disableFollowRedirect
Building Complex Scenarios
Scenario Composition
// Reusable scenario components
val browse = exec(
http("Homepage")
.get("/")
.check(status.is(200))
).pause(2, 5) // Random pause between 2-5 seconds
val search = exec(
http("Search Products")
.get("/search")
.queryParam("q", "laptop")
.check(status.is(200))
.check(jsonPath("$.results[*].id").findAll.saveAs("productIds"))
).pause(1, 3)
val viewProduct = exec(
http("View Product")
.get("/products/${productId}")
.check(status.is(200))
).pause(3, 7)
// Compose into full scenario
val scn = scenario("E-commerce User Journey")
.exec(browse)
.exec(search)
.exec(session => {
val productIds = session("productIds").as[Vector[String]]
val randomId = productIds(scala.util.Random.nextInt(productIds.length))
session.set("productId", randomId)
})
.exec(viewProduct)
Conditional Execution
val scn = scenario("Conditional Flow")
.exec(
http("Get User")
.get("/users/me")
.check(status.is(200))
.check(jsonPath("$.role").saveAs("userRole"))
)
.doIf(session => session("userRole").as[String] == "admin") {
exec(
http("Admin Dashboard")
.get("/admin/dashboard")
.check(status.is(200))
)
}
.doIfOrElse(session => session("userRole").as[String] == "premium") {
exec(http("Premium Features").get("/premium"))
} {
exec(http("Standard Features").get("/standard"))
}
Loops and Iterations
val scn = scenario("Loop Example")
// Repeat fixed number of times
.repeat(5) {
exec(http("Request ${counter}").get("/api/data"))
.pause(1)
}
// Repeat while condition is true
.asLongAs(session => session("continue").as[Boolean]) {
exec(http("Poll").get("/api/status"))
.pause(5)
}
// Repeat for duration
.during(30.seconds) {
exec(http("Sustained Load").get("/api/endpoint"))
.pause(1)
}
// Iterate over collection
.foreach(List("product1", "product2", "product3"), "productId") {
exec(
http("Get Product ${productId}")
.get("/products/${productId}")
)
}
Request Building and Validation
POST Requests with Body
val scn = scenario("API CRUD Operations")
// JSON body from string
.exec(
http("Create User")
.post("/users")
.body(StringBody("""{"name": "John", "email": "john@example.com"}"""))
.asJson
.check(status.is(201))
.check(jsonPath("$.id").saveAs("userId"))
)
// JSON body from template
.exec(
http("Create Order")
.post("/orders")
.body(ElFileBody("templates/order.json"))
.asJson
)
// Form data
.exec(
http("Submit Form")
.post("/submit")
.formParam("field1", "value1")
.formParam("field2", "${dynamicValue}")
)
Advanced Checks
val scn = scenario("Response Validation")
.exec(
http("Complex Validation")
.get("/api/products")
.check(status.is(200))
// JSON path checks
.check(jsonPath("$.products[*]").count.gte(10))
.check(jsonPath("$.products[0].name").exists)
.check(jsonPath("$.products[*].price").findAll.saveAs("prices"))
// Response time check
.check(responseTimeInMillis.lte(500))
// Header checks
.check(header("Content-Type").is("application/json"))
// Body substring check
.check(substring("success").exists)
// Regular expression
.check(regex("""id":\s*(\d+)""").findAll.saveAs("ids"))
// MD5/SHA hash verification
.check(md5.is("expected-hash"))
)
Session Management
val scn = scenario("Session Variables")
.exec { session =>
println(s"Session ID: ${session.userId}")
session.set("customVar", "value")
}
.exec(
http("Login")
.post("/login")
.body(StringBody("""{"username": "${username}", "password": "${password}"}"""))
.check(jsonPath("$.token").saveAs("authToken"))
)
.exec(
http("Authenticated Request")
.get("/protected")
.header("Authorization", "Bearer ${authToken}")
)
// Access session data
.exec { session =>
val token = session("authToken").as[String]
println(s"Using token: $token")
session
}
Feeders for Data-Driven Tests
CSV Feeder
// users.csv:
// username,password,role
// user1,pass1,admin
// user2,pass2,user
val csvFeeder = csv("users.csv").random // Random selection
val csvFeederCircular = csv("users.csv").circular // Round-robin
val csvFeederQueue = csv("users.csv").queue // Sequential, one-time
val scn = scenario("Data-Driven Login")
.feed(csvFeeder)
.exec(
http("Login as ${username}")
.post("/login")
.body(StringBody("""{"username": "${username}", "password": "${password}"}"""))
.check(status.is(200))
)
JSON and Custom Feeders
// JSON feeder
val jsonFeeder = jsonFile("products.json").random
// Array feeder
val customFeeder = Array(
Map("product" -> "Laptop", "price" -> "999"),
Map("product" -> "Phone", "price" -> "599")
).random
// Generated feeder
val idFeeder = Iterator.continually(
Map("userId" -> scala.util.Random.nextInt(1000))
)
val scn = scenario("Custom Feeder")
.feed(idFeeder)
.exec(
http("Get User ${userId}")
.get("/users/${userId}")
)
Real-World Testing Scenarios
E-commerce Complete User Flow
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class EcommerceSimulation extends Simulation {
val httpProtocol = http
.baseUrl("https://ecommerce.example.com")
.acceptHeader("application/json")
val csvFeeder = csv("users.csv").circular
// Scenario: Browse and Purchase
val browse = scenario("Browse Products")
.feed(csvFeeder)
.exec(
http("Homepage")
.get("/")
.check(status.is(200))
)
.pause(2, 4)
.exec(
http("Category Page")
.get("/category/electronics")
.check(status.is(200))
.check(jsonPath("$.products[*].id").findAll.saveAs("productIds"))
)
.pause(3, 6)
val purchase = scenario("Complete Purchase")
.feed(csvFeeder)
.exec(browse)
.exec { session =>
val productIds = session("productIds").as[Vector[String]]
session.set("selectedProduct", productIds.head)
}
.exec(
http("Product Details")
.get("/products/${selectedProduct}")
.check(status.is(200))
)
.pause(5, 10)
.exec(
http("Add to Cart")
.post("/cart")
.body(StringBody("""{"productId": "${selectedProduct}", "quantity": 1}"""))
.asJson
.check(status.is(200))
)
.pause(2, 5)
.exec(
http("Checkout")
.post("/checkout")
.body(ElFileBody("checkout.json"))
.asJson
.check(status.is(201))
)
// Load simulation
setUp(
browse.inject(
rampUsers(100) during (2.minutes)
),
purchase.inject(
rampUsers(50) during (3.minutes)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile3.lt(1500),
global.successfulRequests.percent.gt(95)
)
}
API Microservices Load Test
class MicroservicesSimulation extends Simulation {
val httpProtocol = http.baseUrl("https://api.example.com")
// User service scenario
val userService = scenario("User Service")
.exec(
http("Create User")
.post("/users")
.body(StringBody("""{"name": "Test User", "email": "test@example.com"}"""))
.asJson
.check(status.is(201))
.check(jsonPath("$.id").saveAs("userId"))
)
.exec(
http("Get User")
.get("/users/${userId}")
.check(status.is(200))
)
// Product service scenario
val productService = scenario("Product Service")
.repeat(10) {
exec(
http("List Products")
.get("/products")
.queryParam("page", "${page}")
.check(status.is(200))
)
}
// Order service scenario
val orderService = scenario("Order Service")
.exec(
http("Create Order")
.post("/orders")
.body(StringBody("""{"userId": 1, "items": [{"productId": 1, "quantity": 2}]}"""))
.asJson
.check(status.is(201))
.check(jsonPath("$.orderId").saveAs("orderId"))
)
.exec(
http("Get Order Status")
.get("/orders/${orderId}/status")
.check(status.is(200))
)
setUp(
userService.inject(constantUsersPerSec(10) during (5.minutes)),
productService.inject(constantUsersPerSec(50) during (5.minutes)),
orderService.inject(rampUsersPerSec(5) to 20 during (2.minutes))
).protocols(httpProtocol)
.maxDuration(10.minutes)
}
CI/CD Integration
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git 'https://github.com/your/repo.git'
}
}
stage('Run Gatling Tests') {
steps {
sh 'mvn gatling:test'
}
}
stage('Publish Reports') {
steps {
gatlingArchive()
}
}
}
post {
always {
publishHTML([
reportDir: 'target/gatling',
reportFiles: 'index.html',
reportName: 'Gatling Report'
])
}
}
}
GitHub Actions
name: Performance Tests
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
gatling:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run Gatling Tests
run: mvn gatling:test
- name: Upload Gatling Results
uses: actions/upload-artifact@v3
if: always()
with:
name: gatling-results
path: target/gatling/
GitLab CI
gatling-tests:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- mvn gatling:test
artifacts:
when: always
paths:
- target/gatling/
expire_in: 1 week
only:
- schedules
- main
Assertions and SLAs
setUp(
scn.inject(rampUsers(100) during (1.minute))
).protocols(httpProtocol)
.assertions(
// Global assertions
global.responseTime.max.lt(5000),
global.responseTime.mean.lt(1000),
global.responseTime.percentile3.lt(1500), // 95th percentile
global.responseTime.percentile4.lt(2000), // 99th percentile
// Success rate
global.successfulRequests.percent.gt(99),
global.failedRequests.count.lt(10),
// Request-specific assertions
forAll.responseTime.max.lt(3000),
details("Homepage").responseTime.max.lt(500),
details("API Call").successfulRequests.percent.is(100)
)
Best Practices
Simulation Organization
// BaseSimulation.scala - Common configuration
abstract class BaseSimulation extends Simulation {
val httpProtocol = http
.baseUrl(System.getProperty("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
.userAgentHeader("Gatling")
val defaultPause = 1.second
}
// Specific simulations extend base
class UserSimulation extends BaseSimulation {
// Simulation-specific code
}
Think Time and Pacing
val scn = scenario("Realistic User")
.exec(http("Action 1").get("/page1"))
.pause(2, 5) // Random between 2-5 seconds
.exec(http("Action 2").get("/page2"))
.pace(10.seconds) // Maintain 10s between iterations
.exec(http("Action 3").get("/page3"))
.rendezVous(100) // Wait until 100 users reach this point
Conclusion
Gatling combines high performance with developer productivity, making it an excellent choice for modern performance testing. Its Scala DSL provides type safety and expressiveness, while its async architecture delivers exceptional load generation capabilities.
Key strengths:
- High-performance async architecture
- Expressive Scala DSL with IDE support
- Beautiful, detailed HTML reports
- Excellent protocol support
- Strong CI/CD integration
- Enterprise-ready features
Whether testing REST APIs, microservices, or full web applications, Gatling provides the performance, features, and reporting necessary for comprehensive load testing (as discussed in Artillery Performance Testing: Modern Load Testing with YAML Scenarios) in modern DevOps environments.