Testland
Browse all skills & agents

gatling-load-testing

Authors Gatling simulations in Java / Kotlin / Scala (or JS / TS) using the Simulation class plus http() / scenario() / exec() DSL builders, ramps virtual users via injectOpen (arrival rate) or injectClosed (concurrent count), runs via Maven / Gradle / sbt with the Gatling plugin, and gates CI on assertions defined in setUp(). Use when the project is on the JVM and the team prefers code-first load tests over JMeter's XML or k6's JavaScript-only authoring.

Install with skills.sh (any agent)

npx skills add testland/qa --skill gatling-load-testing
View source

gatling-load-testing

Gatling tests are Simulation classes in Java / Kotlin / Scala / JS / TS that compose http() / scenario() / exec() DSL builders and run via the Gatling Maven / Gradle / sbt plugin (per gatling-tutorial (opens in new window)). Supported protocols span HTTP, WebSocket, Server-Sent Events, JMS, gRPC, and MQTT (gatling-readme (opens in new window)).

When to use

  • The team is on the JVM and wants type-safe code-first load tests (vs. JMeter's XML or k6's JS).
  • The project needs non-HTTP protocols (WebSocket, JMS, gRPC, MQTT) - Gatling's first-party support is broader than k6's.
  • A team value is scenario expressiveness - Gatling's DSL composes naturally for multi-step user journeys with shared state.
  • The project already uses Maven / Gradle / sbt; the Gatling plugin integrates cleanly.

For pure HTTP load testing on a non-JVM stack, prefer k6-load-testing (JavaScript) or locust-load-testing (Python).

Install

The current version + matching plugin is documented at docs.gatling.io (opens in new window) - pin to a specific release rather than LATEST. The minimum dependencies for a Maven project are the Gatling Maven plugin (build) plus gatling-charts-highcharts (test scope, for HTML report generation).

For Gradle / sbt setups, the equivalent plugins are gatling-gradle-plugin and sbt-gatling. See gatling-tutorial (opens in new window) for the canonical project-init flow.

Authoring

Simulation class structure

Per gatling-tutorial (opens in new window), every Gatling test extends Simulation and uses three DSL builders:

BuilderPurpose
http(...)HTTP protocol config: base URL, default headers, share-connection settings.
scenario(...)A named sequence of user actions.
exec(...)Executes one request or a chain of actions within a scenario.

Java example:

package com.example.load;

import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class OrdersSimulation extends Simulation {

  HttpProtocolBuilder httpProtocol = http
    .baseUrl("https://staging.example.com")
    .acceptHeader("application/json")
    .header("Authorization", "Bearer " + System.getenv("API_TOKEN"));

  ScenarioBuilder ordersScenario = scenario("Order lifecycle")
    .exec(
      http("Create order")
        .post("/orders")
        .body(StringBody("{\"sku\":\"SKU-1\",\"qty\":2}"))
        .check(status().is(201))
        .check(jsonPath("$.order_id").saveAs("orderId"))
    )
    .pause(1)
    .exec(
      http("Read order")
        .get("/orders/#{orderId}")
        .check(status().is(200))
    );

  {
    setUp(
      ordersScenario.injectOpen(
        rampUsersPerSec(1).to(20).during(Duration.ofMinutes(1)),
        constantUsersPerSec(20).during(Duration.ofMinutes(2))
      )
    )
    .protocols(httpProtocol)
    .assertions(
      global().responseTime().percentile(95).lt(500),
      global().failedRequests().percent().lt(1.0)
    );
  }
}

(Adapted from gatling-tutorial (opens in new window) DSL primitives.)

Injection profiles

Per gatling-tutorial (opens in new window):

Open workload - injectOpen

New users arrive continuously during the test window. Use when modeling realistic traffic that doesn't depend on user response time.

ordersScenario.injectOpen(
  nothingFor(Duration.ofSeconds(5)),                        // warmup grace
  rampUsersPerSec(1).to(50).during(Duration.ofMinutes(1)),  // ramp to 50 RPS over 1 min
  constantUsersPerSec(50).during(Duration.ofMinutes(5))     // hold 50 RPS for 5 min
)

Closed workload - injectClosed

A fixed pool of users repeats actions. Use when modeling sessions / connection-pool behavior where total concurrency matters more than arrival rate.

ordersScenario.injectClosed(
  rampConcurrentUsers(0).to(100).during(Duration.ofMinutes(1)),
  constantConcurrentUsers(100).during(Duration.ofMinutes(5))
)

Most public APIs are Open; session-bound systems (databases, video streams) are Closed.

Assertions

Per gatling-tutorial (opens in new window), setUp().assertions(...) defines the CI gate criteria. Every assertion is a chain of selectors:

SelectorWhat it asserts
global().responseTime().percentile(95).lt(500)Global p95 response time < 500 ms.
global().failedRequests().percent().lt(1.0)< 1% of requests failed globally.
details("Create order").requestsPerSec().gte(20)Specific request name throughput.
forAll().responseTime().mean().lt(300)Mean across every named request < 300ms.

Failed assertions cause Gatling to exit non-zero - the canonical CI gate.

Running

Maven

mvn gatling:test                                                # runs all simulations
mvn gatling:test -Dgatling.simulationClass=com.example.load.OrdersSimulation

The plugin places HTML reports under target/gatling/<simulation>-<timestamp>/.

Gradle

./gradlew gatlingRun                                            # runs all
./gradlew gatlingRun-com.example.load.OrdersSimulation          # runs one

sbt (Scala)

sbt 'Gatling/test'

Reports

Per gatling-readme (opens in new window), each run produces an HTML report under <output>/<simulation>-<timestamp>/index.html with:

  • Per-request response-time distributions and percentiles.
  • Throughput timeline.
  • Active-users-over-time chart.
  • Pass/fail status per assertion.

For machine-readable output, parse <output>/.../js/stats.json - contains the same data the HTML report renders.

CI integration

Full GitHub Actions workflow (PR + nightly, report uploaded via if: always()) in references/ci-integration.md. A failed assertion exits the Maven build non-zero.

Anti-patterns

See references/anti-patterns.md: wrong injection model for the traffic, hardcoded URLs/tokens, missing pause(), asserting only failedRequests, synthetic spikes, and per-iteration re-authentication.

Limitations

  • JVM only for native execution. JS / TS / Kotlin DSL are available but compile down to JVM bytecode under the hood.
  • Per-machine VU limits. A single Gatling instance saturates one machine's outbound capacity; for higher loads, distribute via the open-source distributed mode or use Gatling Enterprise.
  • DSL learning curve. Compared to k6's JavaScript, the Simulation class shape and instance-initializer block are unfamiliar to JS developers.

References

  • gatling-readme (opens in new window) - main repo: positioning, language support, supported protocols.
  • gatling-tutorial (opens in new window) - DSL primitives: Simulation class, http() / scenario() / exec(), injectOpen vs injectClosed, setUp().assertions().
  • k6-load-testing, jmeter-load-testing, locust-load-testing - alternatives by stack.
  • perf-budget-gate - downstream gate that aggregates load-runner verdicts with frontend perf metrics.

Gatling anti-patterns

Common Gatling simulation mistakes and their fixes. Each one produces numbers that either fail to model production or corrupt the metrics outright.

Anti-patternWhy it failsFix
Using injectClosed for an open-traffic APIModels the wrong system; results don't predict prod behavior.Match the workload model: open API -> injectOpen; session-bound -> injectClosed.
Hardcoded URLs / tokens in the SimulationTests bind to one environment.System.getenv("API_BASE_URL") + System.getenv("API_TOKEN").
Missing pause() between requestsHammering at full rate doesn't model real users.pause(1) or pause(Duration.ofSeconds(1), Duration.ofSeconds(3)) for randomized think time.
Asserting only failedRequestsA 30-second response that succeeds passes the gate but breaks UX.Always pair with percentile latency assertions.
Open-workload with rampUsersPerSec(0).to(1000) over 10sSynthetic spike; not realistic; client-side bottlenecks corrupt metrics.Realistic warm-up then sustained load; spike tests are a separate scenario.
Saving auth-token discovery inside the scenarioEach VU re-authenticates on every iteration; auth endpoint becomes the bottleneck.Authenticate once in before { ... } block; share the token across the whole simulation.

Gatling CI integration

View source (opens in new window)

Gatling CI integration

A GitHub Actions workflow that runs the Gatling Maven build on pull requests that touch simulation files and on a nightly schedule. A failed setUp().assertions(...) exits the Maven build non-zero and fails the job; the HTML report is uploaded regardless via if: always().

# .github/workflows/gatling.yml
name: load-test

on:
  pull_request:
    paths: ['src/test/java/**/*Simulation.java']
  schedule:
    - cron: '0 4 * * *'

jobs:
  gatling:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'
          cache: 'maven'

      - name: Run Gatling
        env:
          API_TOKEN: ${{ secrets.STAGING_API_TOKEN }}
        run: mvn -B gatling:test

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: gatling-report
          path: target/gatling/
          retention-days: 14

For Gradle, swap the run step for ./gradlew gatlingRun; for sbt, sbt 'Gatling/test'.

Related skills

db-query-plan-analyzer

Reads `EXPLAIN` / `EXPLAIN ANALYZE` output from PostgreSQL, MySQL, or SQLite - identifies the dominant cost (sequential scan, nested loop, sort spill, missing index, type-cast preventing index use), proposes the specific index or query rewrite to fix it, and emits the candidate `CREATE INDEX` statement. Use when load testing or production telemetry shows the database as the bottleneck and the team needs targeted query-level remediation.

flame-graph-analyzer

Reads CPU flame-graph output from py-spy (Python), async-profiler (JVM), Go pprof, or Node.js `perf_hooks` / clinic.js: identifies the hot path (top sample-time frames), classifies the bottleneck (CPU-bound vs lock contention vs allocator pressure), and proposes the next investigation step. Use when a perf regression is bisected to a commit but the hot path inside it is unclear; for tail-latency percentiles use latency-percentile-analyzer, for GC pauses specifically use jvm-gc-tuning, and for a slow SQL hot path use db-query-plan-analyzer.

jmeter-load-testing

Authors Apache JMeter `.jmx` test plans (Thread Groups + HTTP samplers + assertions + listeners) in the JMeter GUI, runs them headlessly via `jmeter -n -t plan.jmx -l results.jtl`, generates an HTML dashboard with `-e -o`, and gates CI on JTL parsing. Use when the project has an existing JMeter investment, needs JVM-native load tooling, or works in domains with strong JMeter community support (banking, telecom, enterprise).

jvm-gc-tuning

Diagnoses JVM garbage-collection behaviour under load: reads and interprets unified GC logs (-Xlog:gc*), selects the right collector (G1 vs ZGC vs Parallel vs Serial), tunes heap sizing and pause-time targets, quantifies allocation rate, and traces the GC-pause-to-latency-tail link using GCViewer and Java Flight Recorder (JFR). Use when a load test reveals p99/p999 latency spikes that correlate with GC activity, or when heap sizing and collector selection need justification before a performance baseline is locked.

k6-load-testing

Authors k6 JavaScript load-test scripts (VU loops + checks + sleeps), configures the `options` block with `stages` (ramp-up patterns) and `thresholds` (p(95) latency, error rate), runs via `k6 run script.js` or `--vus / --duration` ad-hoc flags, and uses thresholds as the CI pass/fail signal. Use when the project ships HTTP / WebSocket / gRPC load tests and the team wants developer-friendly JavaScript authoring.

latency-percentile-analyzer

Interprets latency distributions from k6 load tests beyond the p95/p99 gate: reads percentile summaries and JSON exports to identify tail shape, computes the tail ratio (p99/p50) as a distribution-spread signal, detects bimodal distributions, explains coordinated omission and why naive p99 values are optimistic under sustained load, and distinguishes request-rate from concurrency models. Use when a k6 threshold passes but the system still feels slow, when p99 is suspiciously low during ramp-up, or when the team needs to explain why tail latency is high rather than just observing that it is.

lighthouse-budget-author

Drafts a `lighthouserc.js` (or `budget.json`) at design time - picks Web Vitals thresholds (LCP / INP / CLS) per route based on traffic class (cached / dynamic / API-heavy / form-heavy) and the team's NFRs, plus resource-size budgets (JS / CSS / images / total bytes). Emits the config file ready for the lighthouse-perf runner. Use when starting Lighthouse coverage on a project that has no budgets yet, or when the existing budgets need a redesign.

lighthouse-perf

Configures Lighthouse CI (`@lhci/cli`) to audit Web Vitals (LCP, INP, CLS) on every PR, asserts against canonical thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1 at the 75th percentile), uploads Lighthouse reports as build artifacts, and posts deltas as PR comments. Use when the project ships a web frontend and the team needs continuous Web Vitals monitoring tied to PR gating.

load-testing-overview

Teaches load and performance testing from zero: how to choose between k6, JMeter, Gatling, Locust, and Artillery based on observable project facts (team language, tests-as-code vs GUI authoring, protocols beyond HTTP, CI gating needs); the six load profiles (smoke, average-load, stress, spike, soak, breakpoint) and the question each one answers; the difference between open workload models that hold arrival rate constant and closed models that hold concurrent users constant; why percentiles rather than averages are the unit of measurement; and how to turn a run into a pass/fail CI gate, with a first runnable k6 script. Use when a service needs performance coverage and the tool, the load profile, or the pass/fail threshold has not been decided yet.

locust-load-testing

Authors Locust load tests as Python classes - HttpUser with @task-decorated methods plus on_start hooks and between() wait_time - runs via `locust -f locustfile.py` headless mode (or distributed via `--master` / `--worker`), and exports CSV / JUnit reports for CI gating. Use when the project's primary stack is Python and the team wants load tests in the same language as the application.

perf-budget-gate

Builds a unified release-readiness gate that aggregates verdicts from any combination of k6 / JMeter / Gatling / Locust load runners and Lighthouse CI Web Vitals, applies severity-aware pass/fail thresholds, and emits a single go / no-go decision with per-metric deltas vs the main-branch baseline. Posts the delta as a PR comment when the team has the integration set up. Use when authoring a CI step that gates a deployment on cross-runner perf compatibility.

slo-load-test-plan

Turns a service's SLOs and endpoint traffic mix into a named scenario matrix: one scenario per SLO boundary condition, a load profile (smoke, average-load, stress, soak, spike, breakpoint) per scenario, an open or closed workload injection model, a threshold expression derived from the SLO the scenario guards, and an error-budget calculation that sets the soak run's failure allowance. Stays runner-agnostic and fixes the pass/fail line before any tool is configured. Use when an SLO document and an endpoint list both exist but nobody has decided which load runs to make, what shape of load each carries, or what number would count as a failure.