by thetestingacademy
Apache JMeter load testing with thread groups, assertions, and distributed testing
npx @qaskills/cli add jmeter-loadAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
You are an expert performance engineer specializing in Apache JMeter. When the user asks you to create, review, or debug JMeter test plans, follow these detailed instructions.
jmeter/
test-plans/
smoke-test.jmx
load-test.jmx
stress-test.jmx
api-test.jmx
data/
users.csv
products.csv
payloads/
create-order.json
lib/
custom-plugins.jar
scripts/
run-load-test.sh
generate-report.sh
results/
.gitkeep
reports/
.gitkeep
jmeter.properties
A well-organized JMeter test plan follows this hierarchy:
Test Plan
├── User Defined Variables
├── HTTP Request Defaults
├── HTTP Header Manager
├── HTTP Cookie Manager
├── CSV Data Set Config
├── Thread Group (User Flow)
│ ├── Transaction Controller (Login)
│ │ ├── HTTP Request (GET /login)
│ │ ├── HTTP Request (POST /auth/login)
│ │ ├── Response Assertion
│ │ ├── JSON Extractor (token)
│ │ └── JSR223 PostProcessor
│ ├── Constant Timer (Think Time)
│ ├── Transaction Controller (Browse Products)
│ │ ├── HTTP Request (GET /products)
│ │ └── Response Assertion
│ └── Transaction Controller (Checkout)
│ ├── HTTP Request (POST /cart)
│ ├── HTTP Request (POST /checkout)
│ └── Response Assertion
├── View Results Tree (debug only)
├── Summary Report
└── Backend Listener (InfluxDB)
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Load Test Users">
<intProp name="ThreadGroup.num_threads">100</intProp>
<intProp name="ThreadGroup.ramp_time">300</intProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">1800</stringProp>
<stringProp name="ThreadGroup.delay">0</stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
</ThreadGroup>
Use the Ultimate Thread Group plugin for complex ramp patterns:
Example pattern for a load test:
| Start | Delay | Startup | Hold | Shutdown |
|---|---|---|---|---|
| 25 | 0s | 60s | 300s | 30s |
| 25 | 60s | 60s | 240s | 30s |
| 25 | 120s | 60s | 180s | 30s |
| 25 | 180s | 60s | 120s | 30s |
Always configure HTTP Request Defaults at the test plan level:
Protocol: https
Server Name: ${BASE_URL}
Port Number: ${PORT}
Content Encoding: UTF-8
Implementation: HttpClient4
Connect Timeout: 5000
Response Timeout: 30000
Extract values from JSON responses:
Variable Names: auth_token
JSON Path Expressions: $.token
Match No.: 1
Default Values: NOT_FOUND
For HTML or non-JSON responses:
Reference Name: csrf_token
Regular Expression: name="csrf_token" value="(.+?)"
Template: $1$
Match No.: 1
Default Value: NOT_FOUND
Simpler alternative to regex:
Reference Name: session_id
Left Boundary: sessionId=
Right Boundary: ;
Match No.: 1
In subsequent requests:
Header: Authorization: Bearer ${auth_token}
URL Path: /api/users/${user_id}
Body: {"sessionId": "${session_id}"}
Apply to: Main sample only
Field to Test: Response Code
Pattern Matching Rules: Equals
Patterns to Test: 200
Assert JSON Path exists: $.data.id
Expected Value: (leave empty to just check existence)
Additionally assert value: false
Duration in milliseconds: 2000
Apply to: Main sample only
Size to Assert: Response body
Type of Comparison: < (less than)
Size in bytes: 1048576
Thread Delay: 1000
More realistic than constant timers:
Deviation: 500
Constant Delay Offset: 2000
This produces delays between ~1000ms and ~3000ms with most around 2000ms.
Random Delay Maximum: 3000
Constant Delay Offset: 1000
Produces delays between 1000ms and 4000ms uniformly distributed.
Filename: data/users.csv
File Encoding: UTF-8
Variable Names: username,password,role
Ignore first line: true
Delimiter: ,
Allow quoted data: true
Recycle on EOF: true
Stop thread on EOF: false
Sharing mode: All threads
username,password,role
user1@example.com,Pass123!,user
user2@example.com,Pass456!,user
admin@example.com,Admin789!,admin
import java.time.Instant
import java.util.UUID
vars.put("request_id", UUID.randomUUID().toString())
vars.put("timestamp", Instant.now().toString())
vars.put("random_email", "user_${__Random(1000,9999)}@example.com")
// Generate random order amount
def amount = (Math.random() * 1000 + 10).round(2)
vars.put("order_amount", amount.toString())
import groovy.json.JsonSlurper
def response = prev.getResponseDataAsString()
def json = new JsonSlurper().parseText(response)
if (json.data && json.data.size() > 0) {
def firstItem = json.data[0]
vars.put("product_id", firstItem.id.toString())
vars.put("product_name", firstItem.name)
log.info("Extracted product: ${firstItem.name}")
} else {
log.warn("No products found in response")
prev.setSuccessful(false)
prev.setResponseMessage("No products in response")
}
import groovy.json.JsonSlurper
def response = prev.getResponseDataAsString()
def json = new JsonSlurper().parseText(response)
// Validate response structure
assert json.data != null : "Response missing 'data' field"
assert json.data.size() > 0 : "Data array is empty"
assert json.total >= json.data.size() : "Total count inconsistent"
// Validate each item
json.data.each { item ->
assert item.id != null : "Item missing ID"
assert item.name?.trim() : "Item missing name"
assert item.price > 0 : "Item price must be positive"
}
On the master machine (jmeter.properties):
remote_hosts=slave1:1099,slave2:1099,slave3:1099
server.rmi.ssl.disable=true
mode=StrippedBatch
On each slave machine:
# Start JMeter server
jmeter-server -Djava.rmi.server.hostname=<slave-ip>
Run distributed test:
jmeter -n -t test-plans/load-test.jmx \
-R slave1,slave2,slave3 \
-l results/distributed-results.jtl \
-e -o reports/distributed-report
# Basic run
jmeter -n -t test-plans/load-test.jmx -l results/results.jtl
# With properties override
jmeter -n -t test-plans/load-test.jmx \
-JBASE_URL=staging.example.com \
-JTHREADS=200 \
-JRAMPUP=300 \
-JDURATION=1800 \
-l results/results.jtl
# Generate HTML report after test
jmeter -g results/results.jtl -o reports/html-report
# Run with HTML report generation
jmeter -n -t test-plans/load-test.jmx \
-l results/results.jtl \
-e -o reports/html-report
# With specific log level
jmeter -n -t test-plans/load-test.jmx \
-l results/results.jtl \
-LDEBUG
For real-time monitoring with Grafana:
Backend Listener Implementation: org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient
influxdbUrl: http://influxdb:8086/write?db=jmeter
application: my-app
measurement: jmeter
summaryOnly: false
samplersRegex: .*
In jmeter.properties or user.properties:
jmeter.save.saveservice.output_format=csv
jmeter.save.saveservice.response_data=false
jmeter.save.saveservice.samplerData=false
jmeter.save.saveservice.requestHeaders=false
jmeter.save.saveservice.url=true
jmeter.save.saveservice.responseHeaders=false
jmeter.save.saveservice.timestamp_format=ms
jmeter.save.saveservice.successful=true
jmeter.save.saveservice.label=true
jmeter.save.saveservice.code=true
jmeter.save.saveservice.message=true
jmeter.save.saveservice.threadName=true
jmeter.save.saveservice.time=true
jmeter.save.saveservice.connect_time=true
jmeter.save.saveservice.latency=true
jmeter.save.saveservice.bytes=true
-Xms1g -Xmx4g for large load tests.- name: Install QA Skills
run: npx @qaskills/cli add jmeter-load10 of 29 agents supported