The most surprising thing about Gatling load testing is that it’s not about simulating users, but rather about simulating network traffic.

Let’s watch Gatling in action. Imagine we’re testing a simple API endpoint that returns a user’s details.

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class UserSimulation extends Simulation {

  val httpProtocol = http
    .baseUrl("http://localhost:8080") // Base URL for all requests
    .acceptHeader("application/json") // Accept JSON responses

  val scn = scenario("User API Test") // Name of the scenario
    .exec(http("Get User") // Name of the request
      .get("/users/123") // HTTP method and path
      .check(status.is(200)) // Assert the status code is 200 OK
      .check(jsonPath("$.id").is("123"))) // Assert the user ID in the JSON response

  setUp(
    scn.inject(
      rampUsers(100) during (10 seconds), // Gradually ramp up to 100 users over 10 seconds
      constantUsersPerSec(50) during (20 seconds) // Maintain 50 users per second for 20 seconds
    ).protocols(httpProtocol)
  )
}

This UserSimulation defines a single scenario that makes a GET request to /users/123. It checks for a 200 OK status and verifies that the returned JSON payload contains a user ID of "123". The setUp block then specifies how to inject load: first, it ramps up to 100 concurrent users over 10 seconds, and then it maintains a steady rate of 50 users per second for another 20 seconds.

Gatling’s Java DSL (Domain Specific Language) allows you to express these load testing scenarios using familiar Java syntax, making it accessible to Java developers. The core components are:

  • Simulation: The entry point for your load test. It defines the scenarios to run and how to inject load.
  • ScenarioBuilder: Defines a sequence of actions (HTTP requests, pauses, etc.) that represent a user’s journey.
  • ChainBuilder: Represents a single step or a series of steps within a ScenarioBuilder. exec() is used to add a ChainBuilder to a ScenarioBuilder.
  • HttpRequestBuilder: Defines an individual HTTP request, including the method (GET, POST, etc.), URL, headers, body, and assertions (checks).
  • HttpProtocolBuilder: Configures the HTTP client, setting base URLs, common headers, timeouts, and other network-level configurations.
  • PopulationBuilder: Defines how users are injected into the simulation over time using methods like rampUsers, constantUsersPerSec, atOnceUsers, etc.

The baseUrl in httpProtocol is crucial; it prevents you from repeating the same domain and port for every request. acceptHeader sets a default Accept header for all requests. The exec block within the scenario defines the actual requests. The .check() methods are where you validate the responses, ensuring your application is behaving as expected under load. The rampUsers and constantUsersPerSec methods in setUp are your primary tools for controlling the load profile, mimicking how users might arrive and interact with your application.

When Gatling runs this simulation, it doesn’t spin up 100 separate JVMs to act as users. Instead, it uses a highly efficient, asynchronous, non-blocking architecture powered by Netty. Each "virtual user" is essentially a state machine that executes a sequence of operations. Gatling manages the lifecycle of these state machines, sending requests and processing responses with minimal overhead. This allows a single Gatling process to simulate tens or even hundreds of thousands of concurrent connections and requests, limited primarily by your machine’s network I/O and CPU.

The check methods are more than just assertions; they are integral to the simulation’s state management. If a check fails, Gatling marks the request as failed and stops executing subsequent steps for that virtual user in that iteration. This mimics a real user encountering an error and likely abandoning their current task.

What many people miss is that Gatling’s statistics are aggregated at the request level. While you define scenarios that look like user journeys, Gatling fundamentally measures the performance of individual HTTP requests. The scenario and population configurations are just ways to drive a specific pattern of these requests over time. This distinction is important because it means you can analyze the performance of specific endpoints independently, even if they are part of a complex user flow.

The next step is understanding how to manage and reuse components like feeders for dynamic data and how to structure more complex, multi-step user journeys.

Want structured learning?

Take the full Gatling course →