JMeter's built-in elements handle most test scenarios — but not all of them. When you need to generate a signed HMAC header, transform a complex JSON response into multiple variables, implement custom retry logic, or call a database to verify a side effect, you need a scripting layer. JSR223 with Groovy is JMeter's answer.
What JSR223 is
JSR223 is the Java Scripting API (Java Specification Request 223) — a standard interface that allows Java applications to embed scripting engines. JMeter uses it to run scripts inside the test plan without compiling a custom plugin.
JMeter supports several JSR223 languages: Groovy, JavaScript (Nashorn), BeanShell (legacy), Jython (Python 2), and others. Use Groovy. It is the only option that compiles and caches scripts at the JVM level, making it as fast as native Java code. BeanShell is interpreted and slow at scale. JavaScript via Nashorn was removed in newer JDKs.
JSR223 element types
- – Full custom sampler — no HTTP call
- – Sets its own SampleResult
- – For non-HTTP protocols or pure logic
- – Use: compute-heavy steps, mocking
- – Runs before parent sampler
- – Modifies vars before request sends
- – Use: sign payloads, build dynamic bodies
- – Runs after parent sampler
- – Reads prev.getResponseDataAsString()
- – Use: complex extraction, data transform
- Custom pass/fail logic –
- Calls prev.setSuccessful(false) –
- Use: business rule validation –
- Returns dynamic delay (ms) –
- Use: calculated think time –
- Example: delay proportional to response size –
Built-in objects available in scripts
JMeter injects these objects into every JSR223 script automatically — no imports needed:
| Object | Type | Purpose |
|---|---|---|
vars | JMeterVariables | Read/write thread-local variables |
props | JMeterProperties | Read/write global properties (all threads) |
prev | SampleResult | The previous sampler's result (Post-Processors and Assertions) |
sampler | AbstractSampler | The current sampler being processed |
ctx | JMeterContext | Thread context: ctx.getThreadNum(), ctx.getVariables() |
log | Logger | SLF4J logger: log.info(), log.error(), log.debug() |
OUT | PrintStream | Writes to stdout (use log instead in most cases) |
Enable "Cache compiled script"
The most important performance setting in any JSR223 element. Check it.
Without caching, Groovy recompiles the script on every execution — for a 100-user test running 500 iterations, that is 50,000 compilations. With caching, the script compiles once and the compiled bytecode is reused. The performance difference under load is 10–100x.
The checkbox appears in every JSR223 element's editor panel, just below the language selector. Enable it whenever the script does not depend on runtime-variable file paths or other dynamic class loading.
Pre-Processor: building dynamic request bodies
// JSR223 Pre-Processor on POST /api/orders
import groovy.json.JsonBuilder
def orderId = "ORD-${System.currentTimeMillis()}-${ctx.getThreadNum()}"
def items = (1..5).collect { [
productId: (int)(Math.random() * 1000) + 1,
quantity: (int)(Math.random() * 4) + 1
]}
def body = new JsonBuilder([
orderId: orderId,
customerId: vars.get("userId"),
items: items,
currency: "USD"
]).toString()
vars.put("orderBody", body)
vars.put("orderId", orderId)The HTTP Request sampler's Body Data tab contains ${orderBody}. The Pre-Processor generates a fresh, unique body before each execution.
Post-Processor: complex extraction and validation
// JSR223 Post-Processor on GET /api/inventory
import groovy.json.JsonSlurper
def body = prev.getResponseDataAsString()
def json = new JsonSlurper().parseText(body)
def items = json.items
// Store aggregate stats
vars.put("itemCount", items.size().toString())
vars.put("totalValue", items.sum { it.price * it.quantity }.toString())
vars.put("outOfStock", items.count { it.quantity == 0 }.toString())
// Store first available item for next request
def available = items.find { it.quantity > 0 }
if (available) {
vars.put("availableId", available.id.toString())
vars.put("availablePrice", available.price.toString())
} else {
vars.put("availableId", "NONE")
log.warn("No available items found in response for thread ${ctx.getThreadNum()}")
}All five variables are immediately available to subsequent samplers in the same thread.
Assertion: custom business rule validation
// JSR223 Assertion on POST /api/transfer
import groovy.json.JsonSlurper
def body = prev.getResponseDataAsString()
def json = new JsonSlurper().parseText(body)
def originalAmt = Double.parseDouble(vars.get("transferAmount"))
def confirmedAmt = json.transaction.amount as Double
def tolerance = 0.01
if (Math.abs(confirmedAmt - originalAmt) > tolerance) {
prev.setSuccessful(false)
prev.setResponseMessage(
"Amount mismatch: sent ${originalAmt}, confirmed ${confirmedAmt}"
)
}prev.setSuccessful(false) marks the sample as failed. prev.setResponseMessage() sets the failure message that appears in listeners and the .jtl file.
Timer: dynamic think time
// JSR223 Timer — returns delay in milliseconds
// Vary think time based on how large the response was
def bytes = prev?.getBodySizeAsLong() ?: 1000L
def readMs = (bytes / 500).toLong() // ~1s per 500 bytes
def minMs = 500L
return Math.max(minMs, readMs)JSR223 Timer must return a numeric value (as long or int) representing the delay in milliseconds. The return statement is the output — not a vars.put().
Groovy in practice: useful patterns
Read a file at runtime (not at startup):
def content = new File(vars.get("dataDir") + "/payload.json").text
vars.put("payload", content)Call an internal utility API to check rate limits before sending:
def url = new URL("http://ratelimit-checker.internal/check?client=${vars.get('clientId')}")
def resp = url.text // simple GET
if (resp.contains('"limited":true')) {
log.info("Rate limited — sleeping 5s")
Thread.sleep(5000)
}Generate an HMAC-SHA256 signature:
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.util.Base64
def secret = props.get("apiSecret")
def payload = vars.get("requestBody")
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secret.bytes, "HmacSHA256"))
vars.put("hmacSignature",
Base64.encoder.encodeToString(mac.doFinal(payload.bytes)))⚠️ Common mistakes
- Forgetting to enable "Cache compiled script". This is the most common JSR223 performance mistake. A Groovy script that runs 10,000 times without caching compiles 10,000 times, consuming significant CPU. Every JSR223 element in a load test should have caching enabled unless there is a specific reason not to.
- Using
vars.put()with non-String values.vars.put()only acceptsStringvalues. Putting an integer:vars.put("count", 5)throws a runtime error. Always convert:vars.put("count", "5")orvars.put("count", items.size().toString()). - Throwing unhandled exceptions in assertions. If a JSR223 Assertion script throws an uncaught exception (for example, a NullPointerException from parsing an unexpected response format), JMeter marks the sample as failed but the error message is the exception stack trace — not your intended failure message. Wrap risky code in try-catch and call
prev.setResponseMessage()explicitly for readable failure output.
🎯 Practice task
Add custom Groovy logic to your test plan.
-
Add a JSR223 Pre-Processor to a POST sampler. Write a script that:
- Builds a JSON body using
groovy.json.JsonBuilder - Includes the current thread number:
ctx.getThreadNum() - Includes a timestamp:
System.currentTimeMillis() - Stores the result in
vars.put("dynamicBody", ...) - Enable "Cache compiled script"
- Builds a JSON body using
-
Set the sampler's Body Data to
${dynamicBody}. Run with 3 users, 2 loops. Check View Results Tree — confirm each request body contains a different thread number and timestamp. -
Add a JSR223 Post-Processor to a GET sampler that returns JSON. Parse the response with
JsonSlurper, count a top-level array (json.someArray.size()), and store the count invars. Log the count withlog.info(). Open the Log Viewer during the run to see the log output. -
Add a JSR223 Assertion. Check whether
prev.getResponseCode()equals"200". If not, callprev.setSuccessful(false)with a descriptive message. Deliberately trigger a non-200 response and confirm the assertion produces your custom message in the Assertion Results listener.