From qa-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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-load-testing:gatling-load-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
[readme]: https://github.com/gatling/gatling
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). Supported protocols span HTTP,
WebSocket, Server-Sent Events, JMS, gRPC, and MQTT
(gatling-readme).
For pure HTTP load testing on a non-JVM stack, prefer
k6-load-testing (JavaScript) or
locust-load-testing (Python).
The current version + matching plugin is documented at
docs.gatling.io - 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 for the canonical project-init flow.
Per gatling-tutorial, every Gatling test extends
Simulation and uses three DSL builders:
| Builder | Purpose |
|---|---|
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 DSL primitives.)
The { ... } initializer block is Java's instance initializer - used
because Simulation's setup happens at construction time.
Per gatling-tutorial:
injectOpenNew 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
)
injectClosedA 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))
)
The choice between Open and Closed is a model decision: most public APIs are Open (users don't wait for a response before sending the next request); session-bound systems (databases, video streams) are Closed.
Per gatling-tutorial, setUp().assertions(...) defines
the CI gate criteria. Every assertion is a chain of selectors:
| Selector | What 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.
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>/.
./gradlew gatlingRun # runs all
./gradlew gatlingRun-com.example.load.OrdersSimulation # runs one
sbt 'Gatling/test'
Per gatling-readme, each run produces an HTML report under
<output>/<simulation>-<timestamp>/index.html with:
For machine-readable output, parse <output>/.../js/stats.json -
contains the same data the HTML report renders.
# .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
A failed assertion exits the Maven build non-zero; the report is
uploaded regardless via if: always().
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Using injectClosed for an open-traffic API | Models the wrong system; results don't predict prod behavior. | Match the workload model: open API → injectOpen; session-bound → injectClosed. |
| Hardcoded URLs / tokens in the Simulation | Tests bind to one environment. | System.getenv("API_BASE_URL") + System.getenv("API_TOKEN"). |
Missing pause() between requests | Hammering at full rate doesn't model real users. | pause(1) or pause(Duration.ofSeconds(1), Duration.ofSeconds(3)) for randomized think time. |
Asserting only failedRequests | A 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 10s | Synthetic 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 scenario | Each VU re-authenticates on every iteration; auth endpoint becomes the bottleneck. | Authenticate once in before { ... } block; share the token across the whole simulation. |
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.npx claudepluginhub testland/qa --plugin qa-load-testingProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.