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

FeatureGatlingJMeterK6Locust
ScriptingScala DSLGUI/XMLJavaScriptPython
PerformanceExcellentMediumExcellentMedium
Resource UsageLowHighLowMedium
Protocol SupportHTTP, WS, JMSExtensiveHTTP/2, WS, gRPCHTTP
ReportingExcellentBasicGoodBasic
RecorderYesYesNoNo
CI/CD IntegrationNativePluginsNativeManual

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.