1. What is JMeter & When to Use It
What is Apache JMeter?
- Open-source load testing tool by Apache Foundation
- Originally for web apps, now supports HTTP, HTTPS, SOAP, REST, FTP, JDBC, LDAP, JMS, TCP, SMTP
- Java-based, extensible via plugins
- GUI for test design, CLI (non-GUI) for execution
- Supports distributed testing across multiple machines
When to Use JMeter
| Scenario | Use JMeter? | Why |
|---|---|---|
| Load testing REST APIs | Yes | Native HTTP sampler, assertions, correlation |
| Stress/spike testing | Yes | Thread groups with ramp-up/scheduling |
| Performance regression in CI/CD | Yes | CLI mode + JTL reports + threshold assertions |
| Database load testing | Yes | JDBC sampler for SQL queries |
| Message queue testing | Yes | JMS publisher/subscriber samplers |
| Browser-based UI testing | No* | Use Selenium/Playwright (*JMeter doesn't render JS) |
| Simple API smoke tests | Maybe | Postman/Newman may be simpler |
| Real user monitoring (RUM) | No | Use browser tools / Lighthouse |
| Synthetic monitoring (scheduled) | Yes | Run on cron, push metrics to Grafana |
| Certificate expiry monitoring | Yes | JSR223 + SSL socket (see Section 8) |
JMeter vs Alternatives
| Tool | Strength | Best For |
|---|---|---|
| JMeter | Feature-rich, extensible, GUI | Enterprise load testing, JDBC/JMS |
| k6 | Dev-friendly (JS), lightweight | API load testing, CI/CD integration |
| Gatling | Scala DSL, good reports | High-concurrency HTTP testing |
| Locust | Python, distributed | Quick scripted load tests |
| wrk/hey | Ultra simple CLI | Quick benchmark, no logic needed |
| Artillery | YAML config, serverless | Cloud-native API testing |
Key JMeter Components Overview
Test Plan
├── Thread Group (virtual users)
│ ├── Config Elements (HTTP defaults, CSV data, cookies)
│ ├── Pre-Processors (JSR223, user params)
│ ├── Timers (constant, gaussian, throughput)
│ ├── Samplers (HTTP, JDBC, JMS, JSR223, TCP)
│ │ ├── Post-Processors (JSON extractor, regex, JSR223)
│ │ └── Assertions (response, duration, JSON, JSR223)
│ └── Logic Controllers (if, loop, transaction, random)
├── Listeners (summary, aggregate, backend)
└── setUp / tearDown Thread Groups2. JMeter Test Plan Structure
Hierarchy: Test Plan → Thread Group → Samplers → Listeners
Key elements: Thread Groups, Samplers (HTTP, JDBC, JMS, etc.), Logic Controllers, Timers, Assertions, Pre/Post Processors, Listeners
Execution Order within a Thread Group
Configuration Elements
Pre-Processors
Timers
Sampler
Post-Processors
Assertions
Listeners⚠️ Important
- Configuration Elements are processed at the start of each iteration
- Pre-Processors run before the sampler in the same scope
- Post-Processors run after the sampler completes
- Listeners collect results but can impact performance
3. Common Annotations / Properties
// Thread Group properties
${__threadNum} // Current thread number
${__threadGroupName} // Thread group name
${__machineIP()} // Machine IP
${__machineName()} // Machine hostname
${__time(yyyy-MM-dd)} // Current date
${__time(HH:mm:ss)} // Current time
${__UUID()} // Generate UUID
${__Random(1,100)} // Random number
${__RandomString(10,abc)} // Random string
// Variables
${__setProperty(name,val,)} // Set global property
${__property(name)} // Read global property
${__V(var_${i})} // Dynamic variable resolution
// Logging in JSR223
log.info("Message: " + vars.get("myVar"))
log.warn("Warning!")
log.error("Error: " + SampleResult.getResponseCode())4. JSR223 / Groovy Scripting
Pre-Processor (JSR223 PreProcessor)
// Set variables before sampler runs
vars.put("token", "Bearer abc123")
vars.put("timestamp", System.currentTimeMillis().toString())
// Access properties
def baseUrl = props.get("base.url") ?: "https://api.example.com"
vars.put("baseUrl", baseUrl)
// Generate dynamic data
import groovy.json.JsonOutput
def payload = JsonOutput.toJson([
name: "test-${__threadNum}",
timestamp: new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'")
])
vars.put("requestBody", payload)Post-Processor (JSR223 PostProcessor)
// Extract from response
def response = prev.getResponseDataAsString()
import groovy.json.JsonSlurper
def json = new JsonSlurper().parseText(response)
vars.put("userId", json.data.id.toString())
vars.put("accessToken", json.token)
// Log response details
log.info("Status: ${prev.getResponseCode()}, Time: ${prev.getTime()}ms")
// Set assertion result dynamically
if (prev.getResponseCode() != "200") {
prev.setSuccessful(false)
prev.setResponseMessage("Expected 200, got ${prev.getResponseCode()}")
}Assertion (JSR223 Assertion)
import groovy.json.JsonSlurper
def json = new JsonSlurper().parseText(prev.getResponseDataAsString())
if (json.status != "success") {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("Expected status 'success', got '${json.status}'")
}
// Check response time
if (prev.getTime() > 2000) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("Response time ${prev.getTime()}ms exceeded 2000ms threshold")
}Timer (JSR223 Timer)
// Dynamic think time based on scenario
def scenario = vars.get("scenario")
switch(scenario) {
case "peak": return 500
case "normal": return 2000
default: return 1000
}5. Metric Types — Which to Use When
| Metric Type | JMeter Element | Use Case | Example |
|---|---|---|---|
| Throughput | Summary Report, Aggregate Report | Requests/sec capacity | API endpoint load test |
| Response Time | Summary Report (Avg/Med/90th/95th/99th) | Latency analysis | Page load SLA validation |
| Error Rate | Summary Report (Error %) | Reliability testing | Error threshold < 1% |
| Concurrent Users | Active Threads Over Time | Scalability testing | Find breaking point |
| Connect Time | Response Times Over Time | Network/TLS overhead | SSL handshake monitoring |
| Bytes Throughput | Bytes Throughput Over Time | Bandwidth testing | File download load test |
| Hits/sec | Hits per Second | Server capacity | CDN/proxy saturation |
| Latency | Response Time Graph | Time to first byte (TTFB) | Backend vs network split |
| TPS | Transactions per Second | Business transaction rate | Checkout flow capacity |
| Percentiles | Aggregate Report (p90/p95/p99) | SLA compliance | 99th percentile < 500ms |
Detailed Use Cases
Load Testing (Capacity)
- Primary: Throughput (req/s), Error Rate
- Secondary: Response Time p95, Active Threads
- Goal: Find max throughput before error rate > threshold
Stress Testing (Breaking Point)
- Primary: Error Rate, Response Time p99
- Secondary: Throughput, Server Resources (via PerfMon)
- Goal: Find when system degrades/fails
Soak/Endurance Testing
- Primary: Memory usage over time, Error Rate trend
- Secondary: Response Time drift, Thread count
- Goal: Detect memory leaks, connection pool exhaustion
Spike Testing
- Primary: Recovery time, Error Rate during spike
- Secondary: Queue depth, Response Time during/after
- Goal: Validate auto-scaling and recovery
API Benchmarking
- Primary: Response Time (avg, median, p95, p99)
- Secondary: Throughput, Error Rate
- Goal: Compare performance across versions/releases
6. Backend Listener — InfluxDB / Grafana Integration
Backend Listener for InfluxDB
<!-- Backend Listener for InfluxDB -->
<BackendListener guiclass="BackendListenerGui" testclass="BackendListener">
<elementProp name="arguments">
<stringProp name="influxdbUrl">http://influxdb:8086/write?db=jmeter</stringProp>
<stringProp name="application">my-api</stringProp>
<stringProp name="measurement">jmeter</stringProp>
<stringProp name="summaryOnly">false</stringProp>
<stringProp name="samplersRegex">.*</stringProp>
<stringProp name="testTitle">Load Test ${__time(yyyy-MM-dd HH:mm)}</stringProp>
</elementProp>
<stringProp name="classname">
org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient
</stringProp>
</BackendListener>Annotate Grafana from JMeter
// JSR223 Sampler at test start/end
import groovy.json.JsonOutput
def annotation = JsonOutput.toJson([
text: "Load Test Started - ${vars.get('testTitle')}",
tags: ["jmeter", "load-test", "${vars.get('environment')}"]
])
def post = new URL("http://grafana:3000/api/annotations").openConnection()
post.setRequestMethod("POST")
post.setRequestProperty("Content-Type", "application/json")
post.setRequestProperty("Authorization", "Bearer ${props.get('grafana.token')}")
post.setDoOutput(true)
post.getOutputStream().write(annotation.getBytes("UTF-8"))
def responseCode = post.getResponseCode()
log.info("Grafana annotation created: ${responseCode}")7. Grafana Annotations from JMeter
Annotating Test Phases
// Mark test phases in Grafana
def createAnnotation(String text, List<String> tags) {
import groovy.json.JsonOutput
def body = JsonOutput.toJson([
dashboardId: 0, // 0 = global annotation
time: System.currentTimeMillis(),
text: text,
tags: tags
])
def conn = new URL("${props.get('grafana.url')}/api/annotations").openConnection()
conn.setRequestMethod("POST")
conn.setRequestProperty("Content-Type", "application/json")
conn.setRequestProperty("Authorization", "Bearer ${props.get('grafana.api.key')}")
conn.setDoOutput(true)
conn.getOutputStream().write(body.getBytes("UTF-8"))
return conn.getResponseCode()
}
// Usage in setUp Thread Group:
createAnnotation("Ramp-up Started: ${__threadGroupName}", ["jmeter", "ramp-up"])
// Usage in tearDown Thread Group:
createAnnotation("Test Complete: ${__threadGroupName}", ["jmeter", "complete"])Annotation Use Cases
🏁 Test start/end markers
Mark when load tests begin and complete
📈 Ramp-up/steady-state/ramp-down
Identify different test phases in dashboards
🚨 Error threshold breaches
Alert when error rate exceeds acceptable limits
🚀 Deployment correlation
Mark when deployments happen to correlate with metrics
8. Certificate Expiry Monitoring with JMeter
Use Case
Monitor SSL/TLS certificate expiry dates across multiple endpoints. Alert when certificates are approaching expiration.
JSR223 Sampler — Certificate Expiry Checker
import javax.net.ssl.*
import java.security.cert.X509Certificate
import java.text.SimpleDateFormat
import groovy.json.JsonOutput
def host = vars.get("target_host") ?: "example.com"
def port = (vars.get("target_port") ?: "443").toInteger()
def warningDays = (vars.get("warning_days") ?: "30").toInteger()
def criticalDays = (vars.get("critical_days") ?: "7").toInteger()
try {
// Create SSL connection and extract certificate
def sslCtx = SSLContext.getInstance("TLS")
def tm = [
checkClientTrusted: { chain, type -> },
checkServerTrusted: { chain, type -> },
getAcceptedIssuers: { null }
] as X509TrustManager
sslCtx.init(null, [tm] as TrustManager[], null)
def factory = sslCtx.getSocketFactory()
def socket = factory.createSocket(host, port) as SSLSocket
socket.startHandshake()
def certs = socket.getSession().getPeerCertificates()
def x509 = certs[0] as X509Certificate
socket.close()
// Calculate days until expiry
def expiry = x509.getNotAfter()
def now = new Date()
def daysUntilExpiry = ((expiry.time - now.time) / (1000 * 60 * 60 * 24)).intValue()
def sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
// Set result variables
vars.put("cert_subject", x509.getSubjectDN().toString())
vars.put("cert_issuer", x509.getIssuerDN().toString())
vars.put("cert_expiry_date", sdf.format(expiry))
vars.put("cert_days_remaining", daysUntilExpiry.toString())
vars.put("cert_serial", x509.getSerialNumber().toString(16))
// Build response
def result = [
host: host, port: port,
subject: x509.getSubjectDN().toString(),
issuer: x509.getIssuerDN().toString(),
notBefore: sdf.format(x509.getNotBefore()),
notAfter: sdf.format(expiry),
daysRemaining: daysUntilExpiry,
serial: x509.getSerialNumber().toString(16),
status: daysUntilExpiry <= criticalDays ? "CRITICAL" :
daysUntilExpiry <= warningDays ? "WARNING" : "OK"
]
SampleResult.setResponseData(
JsonOutput.prettyPrint(JsonOutput.toJson(result)), "UTF-8")
SampleResult.setResponseCode("200")
SampleResult.setResponseMessage(
"Certificate expires in ${daysUntilExpiry} days")
// Mark as failed if critical
if (daysUntilExpiry <= criticalDays) {
SampleResult.setSuccessful(false)
SampleResult.setResponseMessage(
"CRITICAL: Certificate expires in ${daysUntilExpiry} days!")
log.error("CRITICAL: ${host} cert expires in ${daysUntilExpiry} days!")
} else if (daysUntilExpiry <= warningDays) {
log.warn("WARNING: ${host} cert expires in ${daysUntilExpiry} days")
} else {
SampleResult.setSuccessful(true)
log.info("OK: ${host} cert expires in ${daysUntilExpiry} days")
}
} catch (Exception e) {
SampleResult.setSuccessful(false)
SampleResult.setResponseCode("500")
SampleResult.setResponseMessage("Certificate check failed: ${e.message}")
log.error("Certificate check failed for ${host}: ${e.message}")
}Multi-host Monitoring Setup (CSV Data Set Config)
# hosts.csv
target_host,target_port,warning_days,critical_days
api.example.com,443,30,7
web.example.com,443,30,7
mail.example.com,993,30,7
internal.example.com,8443,14,3Grafana Dashboard Integration
- Push cert expiry days as a metric via Backend Listener
- Create Grafana alert:
cert_days_remaining < 14 - Annotation when cert status changes to WARNING/CRITICAL
9. Common Assertions
// Response Code Assertion
// GUI: Add > Assertions > Response Assertion
// Fields to Test: Response Code, Pattern: 200
// Duration Assertion
// GUI: Add > Assertions > Duration Assertion
// Duration (ms): 2000
// JSON Assertion (JSR223)
import groovy.json.JsonSlurper
def json = new JsonSlurper().parseText(prev.getResponseDataAsString())
assert json.data != null : "data field is null"
assert json.data.size() > 0 : "data array is empty"
assert json.status == "OK" : "status is not OK: ${json.status}"
// JSON Path Assertion
// GUI: Add > Assertions > JSON Assertion
// JSON Path: $.data.id
// Expected Value: \d+ (regex)
// Size Assertion
// GUI: Add > Assertions > Size Assertion
// Response Size: > 100 bytes
// XPath Assertion (for XML/SOAP)
// GUI: Add > Assertions > XPath Assertion
// XPath: //status[text()='success']
// Compare Assertion (response vs baseline)
// Useful for regression testing10. Useful CLI Commands
# Run test in non-GUI mode (production)
jmeter -n -t test.jmx -l results.jtl -e -o report/
# Run with properties
jmeter -n -t test.jmx -Jthreads=100 -Jrampup=60 -Jduration=300
# Generate HTML report from existing results
jmeter -g results.jtl -o report/
# Run distributed test
jmeter -n -t test.jmx -R server1,server2,server3
# Heap size (for large tests)
export JVM_ARGS="-Xms2g -Xmx4g"
jmeter -n -t test.jmx -l results.jtl
# Plugin manager CLI
jmeter -Jplugin_manager.command=install \
-Jplugin_manager.plugins="jpgc-graphs-basic"
# Convert JTL to CSV
jmeter -g results.jtl -o /tmp/report \
-Jjmeter.reportgenerator.overall_granularity=100011. Best Practices
- ▸Always run in non-GUI mode for actual tests (
-nflag) - ▸Use CSV Data Set Config for parameterization (not hardcoded values)
- ▸Disable all listeners during load tests (use Backend Listener instead)
- ▸Use Constant Throughput Timer for realistic load patterns
- ▸Correlation: extract tokens/IDs with JSON/RegEx extractors, don't hardcode
- ▸Think time: add realistic delays between requests
- ▸Use Transaction Controllers to group related requests as one business transaction
- ▸Assertions cost performance: keep them minimal in high-load scenarios
- ▸Distribute tests across multiple JMeter slaves for high concurrency
- ▸Version control your .jmx files (they're XML)