Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/gatling/gatling.io-doc/llms.txt

Use this file to discover all available pages before exploring further.

Writing a load test that merely hammers an API endpoint as fast as possible gives you very little actionable data about how your application will behave in production. Realistic load testing requires modelling actual user journeys, injecting diverse test data, adding natural pauses, and choosing the right injection profile to match the traffic pattern you expect. This guide walks through the key practices that separate a useful simulation from one that only floods your server with noise.

Identify User Journeys Before Writing Code

The foundation of every realistic load test is a clear understanding of how real users interact with your application. Before writing a single line of Gatling code, spend time gathering data from your analytics or APM tools.

Product Analytics

Tools such as Amplitude or Firebase reveal which features users actually visit and in what order.

APM Tracing

Datadog, Dynatrace, or similar APM solutions show real request sequences and latency distributions.

Product Owner Input

Subject-matter experts can describe seasonal patterns, power-user workflows, and edge cases analytics may miss.

Network Recording

Use Gatling’s recorder or your browser’s network tab to capture an authentic click-through session.
For an e-commerce site, a typical journey might look like:
  1. User lands on the homepage
  2. User logs in (authentication)
  3. User browses products (authenticated homepage)
  4. User adds a product to the cart
  5. User checks out

Structuring the Project

A well-organised simulation separates concerns into distinct directories so the codebase remains maintainable as it grows.
src/
├── endpoints/          # Individual HTTP request definitions
│   ├── ApiEndpoints    # Backend API calls
│   └── WebEndpoints    # Frontend/page calls
├── groups/             # Named groups of chained requests
│   └── ScenarioGroups
├── utils/              # Helpers, config, constants
│   ├── Config          # System properties / env vars
│   ├── Keys            # Session variable name constants
│   └── TargetEnvResolver
└── resources/
    ├── bodies/         # Request body templates
    └── data/           # Feeder files (CSV, JSON)
Centralise session variable names in a Keys file. Using constants instead of raw strings eliminates typos and makes refactoring much safer across large simulations.

Defining Endpoints

Endpoints are the atomic HTTP request builders referenced throughout your scenarios.

API Endpoint Example — Login

// Java example
HttpRequestActionBuilder login = http("Login")
  .post("/api/auth/login")
  .asFormUrlEncoded()
  .formParam("username", "#{username}")
  .formParam("password", "#{password}")
  .check(status().is(200))
  .check(jmesPath("accessToken").saveAs(Keys.ACCESS_TOKEN));
Key points:
  • #{username} uses the Gatling Expression Language to pull the value from each virtual user’s session (set by a feeder).
  • The .check() step both validates the response (status 200) and extracts the access token for later use.

Web Endpoint Example — Home Page

HttpRequestActionBuilder homePage = http("Home Page")
  .get("https://ecomm.gatling.io")
  .check(status().in(200, 304));
Accepting 304 Not Modified is important — real browsers cache resources, so your test should too.

Using Groups for Logical Separation

Groups bundle related requests so that Gatling Enterprise Edition can report on them as a unit (e.g., “Authenticate” or “Checkout”). This makes it far easier to pinpoint which phase of a user journey is slow.
ChainBuilder authenticate = group("Authenticate").on(
  feed(usersFeeder),
  exec(webEndpoints.loginPage()),
  pause(5, 15),             // Think time: 5–15 seconds
  exec(apiEndpoints.login())
);
The pause(5, 15) call inserts a random pause between 5 and 15 seconds to simulate the time a real user takes to fill in a form. Skipping think times causes your test to run at an unrealistically high request rate.

Building Realistic Scenarios

Flow Control with exitBlockOnFail and randomSwitch

Wrap the entire scenario in exitBlockOnFail so that a virtual user stops as soon as a critical step fails — just as a real user would be unable to proceed after a login error. Use randomSwitch to distribute virtual users across different flows by percentage, mirroring the real traffic split between user segments.
ScenarioBuilder scenarioFrUS = scenario("FR & US Users")
  .exitBlockOnFail(
    exec(authenticate)
    .exec(
      randomSwitch()
        .on(
          percent(70).on(exec(frMarketJourney)),
          percent(30).on(exec(usMarketJourney))
        )
    )
  );

Injection Profiles

The injection profile controls how virtual users arrive. Choosing the wrong profile can make your results meaningless.
Tests how many users the system can sustain. Increments the arrival rate across multiple levels and checks metrics at each step.
rampUsersPerSec(0).to(50).during(Duration.ofMinutes(10))
The profiles above use the open workload model — the injector does not cap concurrency. For closed workload models (e.g., a ticketing website with a fixed queue), use closedModel injection instead.

Defining Assertions

Assertions encode your performance requirements into the simulation so that CI/CD pipelines can fail automatically when standards are not met.
setUp(...)
  .assertions(
    global().responseTime().percentile(99).lt(2000),   // p99 < 2 s
    global().successfulRequests().percent().gt(99.0)   // > 99 % success
  );

Dynamic Configuration via System Properties

Avoid hard-coding test parameters. A Config utility that reads system properties and environment variables lets you change behaviour at run time with no code changes.
// utils/Config.java
public class Config {
  public static final String TEST_TYPE =
      System.getProperty("testType", "smoke");
  public static final String TARGET_ENV =
      System.getProperty("targetEnv", "dev");
}
Launch with the appropriate profile by passing properties on the command line:
# Maven
mvn gatling:test -DtestType=capacity -DtargetEnv=staging

# Gradle
gradle gatlingRun -DtestType=soak -DtargetEnv=production

# JavaScript
npx gatling run testType=breakpoint targetEnv=staging

Authorization Headers Without Repetition

Rather than setting an Authorization header on every individual request, wrap the protocol builder with a conditional helper that checks the virtual user’s session for a token.
private HttpProtocolBuilder withAuthHeaders(HttpProtocolBuilder builder) {
  return builder.header("Authorization", session -> {
    String token = session.getString(Keys.ACCESS_TOKEN);
    return (token != null && !token.isEmpty())
        ? "Bearer " + token
        : "";
  });
}

HttpProtocolBuilder httpProtocol = withAuthHeaders(
  http.baseUrl(targetEnv.baseUrl())
    .acceptHeader("application/json")
    .userAgentHeader("Gatling/LoadTest")
);
This keeps request definitions clean and ensures that every request automatically carries the correct auth header once the user has logged in.

Build docs developers (and LLMs) love