Unit 5.2 · Performance Engineering
JMeter and Gatling (and When to Use Which Tool)
JMeter's thread groups, samplers, and non-GUI runs; Gatling's code-first simulations and injection profiles; distributed load; and choosing between k6, JMeter, Gatling, and Locust.
Start here
The mental model
All load tools do the same three things: describe a user journey, decide how many users arrive and when, and record what happened. They differ in how you write the journey (GUI vs code), which protocols they speak, and how efficiently they generate load. You'll meet JMeter in older enterprise and Java shops, Gatling in JVM teams that want code, and k6 in cloud-native teams. Knowing all three makes you useful anywhere.
Go deeper
How it works inside
01JMeter
A Java desktop tool (since 1998) where a TEST PLAN is a tree: THREAD GROUPS (virtual users, ramp-up, loops or duration) contain SAMPLERS (HTTP request, JDBC, JMS, gRPC via plugins), CONFIG ELEMENTS (CSV data sets, cookie and header managers), TIMERS (think time), ASSERTIONS, and LISTENERS (results). Plans are saved as .jmx XML.
Build and debug in the GUI with a few users; RUN in non-GUI mode (jmeter -n), because the GUI and live listeners consume huge amounts of memory. The HTML dashboard report (-e -o) gives percentiles and throughput over time. Thread groups are closed-model by default; the Concurrency/Arrivals Thread Groups (plugins) and the Throughput Shaping Timer give open-model control. Distributed mode uses one controller and several workers over RMI.
02Gatling
Gatling simulations are code (Java, Kotlin, Scala, or JavaScript/TypeScript), so they live in Git, get reviewed, and reuse functions. Its engine is asynchronous (Netty/Akka-style), so one machine simulates many users cheaply. INJECTION PROFILES are explicit about open vs closed models: rampUsersPerSec(10).to(200).during(3 min) is open; constantConcurrentUsers(100) is closed. ASSERTIONS fail the build like k6 thresholds, and the HTML report is excellent.
03Distributed load and the cloud
When one generator isn't enough (Unit 5.1 drill), spread load across several: JMeter distributed mode, the k6 Operator (runs a test as parallel pods in Kubernetes), Gatling Enterprise, Locust's master/workers, or managed services (Grafana Cloud k6, Azure Load Testing, AWS Distributed Load Testing solution). Aggregate percentiles correctly: you can't average p99s from several generators; you need the merged distributions, which these tools handle.
Do it
Hands-on lab
- 1
Run a JMeter plan headless
A minimal plan (build it in the GUI once, then commit the .jmx).
-Jpasses properties so the same plan runs against any environment and load level.checkout.jmx (excerpt)whole filexml <ThreadGroup testname="Shoppers"> <stringProp name="ThreadGroup.num_threads">${__P(users,50)}</stringProp> <stringProp name="ThreadGroup.ramp_time">${__P(rampup,60)}</stringProp> <boolProp name="ThreadGroup.scheduler">true</boolProp> <stringProp name="ThreadGroup.duration">${__P(duration,300)}</stringProp> </ThreadGroup> <hashTree> <CSVDataSet testname="Users CSV"><stringProp name="filename">users.csv</stringProp></CSVDataSet> <HTTPSamplerProxy testname="GET /products"> <stringProp name="HTTPSampler.domain">${__P(host,localhost)}</stringProp> <stringProp name="HTTPSampler.path">/api/products?q=${query}</stringProp> </HTTPSamplerProxy> <UniformRandomTimer testname="Think time"><stringProp name="RandomTimer.range">2000</stringProp></UniformRandomTimer> </hashTree>terminal$ jmeter -n -t checkout.jmx -Jhost=staging.shoplite.dev -Jusers=200 -Jduration=600 -l results.jtl -e -o report/grep -A1 'summary =' jmeter.log | tail -2── expected output ──summary = 184233 in 00:10:00 = 307.1/s Avg: 188 Min: 12 Max: 4102 Err: 611 (0.33%)Tidying up ... @ Sat Sep 27 11:42:10 IST 2026 - 2
The same journey in Gatling (Java DSL)
An open-model ramp with assertions. Run with the Maven/Gradle plugin; the build fails if assertions fail.
src/test/java/shoplite/CheckoutSimulation.javawhole filejava package shoplite; import static io.gatling.javaapi.core.CoreDsl.*; import static io.gatling.javaapi.http.HttpDsl.*; import io.gatling.javaapi.core.*; import io.gatling.javaapi.http.*; import java.time.Duration; public class CheckoutSimulation extends Simulation { HttpProtocolBuilder http = http.baseUrl(System.getProperty("baseUrl", "http://localhost:8080")) .acceptHeader("application/json"); FeederBuilder<String> users = csv("users.csv").random(); ScenarioBuilder shop = scenario("Browse and checkout") .feed(users) .exec(http("search").get("/api/products?q=#{query}").check(status().is(200))) .pause(Duration.ofSeconds(1), Duration.ofSeconds(3)) .exec(http("add to cart").post("/api/cart").body(StringBody("{\"sku\":\"MUG-RED\"}")).asJson()) .exec(http("checkout").post("/api/checkout").check(status().in(200, 201))); { setUp(shop.injectOpen( rampUsersPerSec(5).to(150).during(Duration.ofMinutes(3)), constantUsersPerSec(150).during(Duration.ofMinutes(5)))) .protocols(http) .assertions( global().responseTime().percentile(95.0).lt(400), global().failedRequests().percent().lt(1.0)); } }terminal$ ./mvnw gatling:test -Dgatling.simulationClass=shoplite.CheckoutSimulation -DbaseUrl=https://staging.shoplite.dev── expected output ──---- Global Information --------------------------------------------------------> request count 91520 (OK=91204 KO=316 )> response time 95th percentile (ms) 361 (OK=355 KO=5001 )> mean requests/sec 190.6 (OK=190 KO=0.66 )Global: 95th percentile of response time is less than 400 : trueGlobal: percentage of failed events is less than 1.0 : true - 3
Distributed k6 on Kubernetes
The k6 Operator runs a script as N parallel pods and splits the load between them.
parallelism: 4quadruples generator capacity.k6-testrun.yamlwhole fileyaml apiVersion: k6.io/v1alpha1 kind: TestRun metadata: name: black-friday-rehearsal spec: parallelism: 4 script: configMap: { name: loadtest, file: loadtest.js } arguments: -e BASE_URL=https://staging.shoplite.dev
Operate it
Knobs that matter
| Setting | Default | What it does | When to change it |
|---|---|---|---|
| JMeter heap (HEAP env) | -Xms1g -Xmx1g | Memory for the JMeter JVM. | Raise for big tests; keep listeners out of non-GUI runs. |
| JMeter -l results.jtl format | CSV | Raw results file. | CSV with only needed fields; avoid saving response data. |
| Gatling injectOpen vs injectClosed | — | Open (arrival rate) vs closed (concurrency) model. | Open for internet-facing services; closed for systems with a fixed user pool (call-centre apps). |
3am practice
Failure drills
Each drill is a real failure mode. Read the scenario and the output, decide what's wrong, then reveal the diagnosis.
Drill #1
JMeter runs out of memory mid-test
A 30-minute test run from the JMeter GUI with a 'View Results Tree' listener stops responding after 8 minutes.
Decide
Choosing a load tool
| Tool | Tests written in | Strengths | Watch out for |
|---|---|---|---|
| k6 | JavaScript (Go engine) | Developer-friendly, thresholds, CI, Grafana, k6 Operator | Not a browser or full Node.js runtime; protocol extensions via xk6 |
| JMeter | GUI → XML (.jmx) | Many protocols (JDBC, JMS, FTP), huge plugin ecosystem, familiar in enterprises | XML in Git is hard to review; heavy per thread |
| Gatling | Java/Kotlin/Scala/JS | Efficient async engine, great reports, code reuse | JVM toolchain; enterprise features are paid |
| Locust | Python | Easy scripting for Python teams, distributed mode | Less efficient per core; closed model by default |
The bigger picture
Connects to
Prove it
Interview questions
Why run JMeter in non-GUI mode?
How do you generate more load than one machine can produce?