Skip to main content

JMeter JSR223 and Groovy Scripting, Tested

JMeter JSR223 and Groovy, tested: cached Groovy ran 55x faster than uncached, a cached ${n} kept its first value, and a failing PostProcessor still passed.

JMeter JSR223 and Groovy Scripting, Tested

JMeter JSR223 elements let you run your own code inside a test plan: a JSR223 Sampler that is itself the request, a PreProcessor or PostProcessor that prepares or reads data around another sampler, an Assertion that decides whether a sample passed, plus a Timer and a Listener. Groovy is the language to use, with “Cache compiled script if available” ticked.

We timed and broke JSR223 scripts on JMeter 5.6.3 with its bundled Groovy 3.0.20. A cached Groovy script cost 86 microseconds per run and the same script uncached 4,709. A cached script that used ${n} returned the first value on every iteration. And a PreProcessor or PostProcessor that threw an exception left its sample green, with the error only in jmeter.log.

Key Takeaways

  • Use Groovy with caching on. In our runs the uncached script was 55 times slower, JavaScript 9 times slower and the BeanShell Sampler 1.6 times slower.
  • Never put ${var} or a ${__function()} call inside a cached script. Read values with vars.get("name") or pass them through the Parameters field.
  • vars holds strings for one thread; props is shared by every thread. vars.put("x", 5) throws an exception, which in a Sampler fails the sample: convert with String.valueOf() or use vars.putObject().
  • An exception in a JSR223 Sampler or Assertion fails the sample. The same exception in a PreProcessor or PostProcessor does not, so check jmeter.log after every run.
  • The first run of a script includes its compilation, and the first Groovy script also pays for starting the Groovy engine: 930 ms in our test, against 65 to 84 ms for a second one-line script. Do not time anything with the wall clock inside a script.

What is JSR223 in JMeter?

JSR223 is the Java standard for running scripting languages inside the JVM, and JMeter uses it for its scriptable test elements. Each JSR223 element takes a language, a script typed into the element or loaded from a file, and an optional Parameters string. JMeter runs the script at that element’s place in the plan, with variables such as vars, props and prev ready to use.

ElementWhen it runsTypical use
JSR223 SamplerAs a sample in its own rightCustom protocol calls, data setup, computed values
JSR223 PreProcessorBefore the sampler it is attached toBuild a request body, sign a request, set variables
JSR223 PostProcessorAfter the sampler it is attached toExtract values from a response, save data, mark a sample failed
JSR223 AssertionAfter the sample and its PostProcessorsPass or fail the sample with a message
JSR223 TimerBefore the samplerA pause computed by the script
JSR223 ListenerAfter each sampleCustom logging or result handling

Where each one sits in the order of execution matters as much as what it does. The guide to the execution order of elements in JMeter shows where PreProcessors, PostProcessors and Assertions fall around a sampler. Which languages JMeter supports, and why BeanShell is on the way out, is covered in which language is used in JMeter.

Why use Groovy for JSR223 scripts?

Groovy is fast in JMeter when its compiled script is cached. With “Cache compiled script if available” ticked, JMeter compiles a Groovy script once and runs the compiled class on every later call. The manual names Groovy as a language that supports this and says Java, BeanShell and JavaScript do not, so those are interpreted on each run.

We ran the same small script 20,000 times on one thread: read a variable, build a 20-step string, write the variable back.

JSR223 Sampler languageTime per run20,000 runs
Groovy, cache on86 µs1.7 s
BeanShell Sampler (not JSR223)137 µs2.7 s
JavaScript (JDK 11)779 µs15.6 s
Groovy, cache off4,709 µs94.2 s

Uncached Groovy was the slowest of all, because it compiled the script again on every run. With the box ticked it was the fastest. Without the cache, every thread that runs the script spends about 4.7 ms compiling it before it can send its next request, and across many threads that adds up on the load generator. The manual also warns that uncached Groovy should run with -Dgroovy.use.classvalue=true because of a Groovy memory leak; keeping the cache on avoids that.

What variables can a JSR223 script use?

JMeter sets up variables before each script runs, so you can use them without declaring anything. The everyday four are vars for this thread’s variables, props for JMeter properties shared by all threads, prev for the last sample’s result in PostProcessors and Assertions, and log for writing to jmeter.log. The table lists the variables the manual documents for the Sampler, the PreProcessor, the PostProcessor and the Assertion.

VariableWhat it isExample
varsThis thread’s JMeter variables (JMeterVariables)vars.get("token"), vars.put("id", "42")
propsJMeter properties, shared by all threadsprops.get("env"), props.put("hits", "1")
prevThe previous SampleResultprev.getResponseDataAsString()
SampleResultThe current sample’s result (Sampler)SampleResult.setResponseCode("299")
AssertionResultThe assertion’s result (Assertion)AssertionResult.setFailure(true)
ctxThe thread’s JMeterContextctx.getThreadNum()
logThe logger, writes to jmeter.loglog.info("token refreshed")
Parameters, argsThe Parameters field as one string, and split on spacesargs[0]
sampler, Label, FileName, OUTThe current sampler, its name, the script file, System.outOUT.println(Label)

How do you get and set JMeter variables in Groovy?

Read a variable with vars.get("name") and write one with vars.put("name", value), where both the name and the value are strings. A Groovy number passed to vars.put throws an exception, so in a Sampler it fails the whole sample. For lists, maps or numbers, use vars.putObject and vars.getObject. Use props only for values every thread must share.

vars.put("x", 5) failed in our run with response code 500 and “groovy.lang.MissingMethodException: No signature of method”, because JMeterVariables.put takes two strings. vars.put("x", String.valueOf(5)) works, and so does vars.putObject("list", [1, 2, 3]), which our script read back with vars.getObject("list").size() as 3.

Variables belong to one thread. In a run with 2 threads, this sampler put a different mine value in each thread, while the property counter it raised under a lock reached 2 for both:

synchronized (props) {
    props.put("hits", ((props.get("hits") ?: 0) as int) + 1)
}
vars.put("mine", ctx.getThreadNum() as String)
return "hits=" + props.get("hits") + " mine=" + vars.get("mine")

The manual’s best-practices page says the same thing: variables are local to a thread by design, and properties are the way to pass a value between threads or thread groups. Values known before the test starts belong in the plan, as User Defined Variables or a CSV file; see the guide to user defined variables in JMeter.

Why does my JSR223 script use the same value every time?

A cached script that contains ${var} or a ${__function()} call keeps the value from its first run. JMeter replaces those references in the script text before compiling, the compiled version is cached, and every later call runs the old value. Read values inside the script with vars.get() instead, or pass them through the Parameters field.

A PreProcessor set n to user-1, user-2, user-3 and user-4 over four iterations, and four samplers returned it in different ways:

Script, cache settingReturned on iterations 1 to 4
return "n=${n}", cache onuser-1 four times
return "n=${n}", cache offuser-1 to user-4
return "n=" + vars.get("n"), cache onuser-1 to user-4
return "n=" + Parameters, Parameters ${n}, cache onuser-1 to user-4

The broken version raised no error and passed every sample, which is why this mistake survives into real test plans. The manual warns about it in its section on the sampler. The Parameters field is the cached-safe way to use JMeter syntax, because JMeter resolves it on every run and hands the result to the script as Parameters and args.

The ${} syntax has a second trap in Groovy. Groovy strings use ${} for their own interpolation, so a JMeter variable that does not exist passes through to Groovy unchanged. Our script with "${__jm__TG__idx}", a variable not defined in that plan, failed all 20,000 samples with “MissingPropertyException: No such property: __jm__TG__idx”. Read it with vars.get() instead, which returns null for a missing variable rather than failing.

How do you write a JSR223 Sampler?

A JSR223 Sampler is a complete sample: the script does the work, the value it returns becomes the response body, and SampleResult sets everything else. The sample passes with code 200 and message “OK” unless the script changes them or throws. An exception fails the sample with code 500 and the exception’s message, which makes failures easy to find in reports.

This sampler sets its own code, message and status:

SampleResult.setResponseCode("299")
SampleResult.setSuccessful(false)
SampleResult.setResponseMessage("custom")
return "body"

It reported response code 299, message “custom”, body “body”, and a failed sample. A sampler that threw new IllegalStateException("boom in sampler") recorded code 500 with “javax.script.ScriptException: java.lang.IllegalStateException: boom in sampler”. The manual adds SampleResult.setIgnore() for a sampler that should not create a result at all, such as one that only prepares data. Generating test data is a common job for this sampler; the guide to generating dynamic IDs in JMeter has examples.

How do you use a JSR223 PostProcessor?

Attach a JSR223 PostProcessor to a sampler to read its response through prev and store what you need in vars. It runs after the sample, before any assertions, so it is the place to parse JSON or other structured responses, save values for later requests, and mark a sample failed when the data is wrong.

This PostProcessor reads a JSON response, stores one field for the next request, and fails the sample when the field is missing:

def json = new groovy.json.JsonSlurper().parseText(prev.getResponseDataAsString())
if (json.peak == null) {
    prev.setSuccessful(false)
    prev.setResponseMessage("no peak in response")
} else {
    vars.put("peak", json.peak as String)
}

prev.setSuccessful(false) with a message failed the sample in our run while the response code stayed 200, so the message is what tells you why. For a single value, JMeter’s JSON Extractor does the same job without code, as described in the guide to correlation in JMeter; use the PostProcessor when the extraction needs logic.

How do you fail a sample from a JSR223 script?

Use a JSR223 Assertion and call AssertionResult.setFailure(true) with a failure message. That is the one way that fails the sample, records why under the assertion’s name, and is reported like any other assertion. A PostProcessor can fail a sample with prev.setSuccessful(false). An exception does not fail it reliably: it depends on which element threw.

def json = new groovy.json.JsonSlurper().parseText(prev.getResponseDataAsString())
if (json.open == null || (json.open as int) > (Parameters as int)) {
    AssertionResult.setFailure(true)
    AssertionResult.setFailureMessage("open=" + json.open + ", limit " + Parameters)
}

We threw a RuntimeException (an IllegalStateException in the Sampler) from each element type to see what the results file recorded:

JSR223 element that threwSample result
SamplerFailed, code 500, exception message
AssertionFailed, assertion marked as an error
PreProcessorPassed; the sampler ran normally
PostProcessorPassed

The last two rows are the dangerous ones. A PreProcessor that crashes while building a request body leaves the sampler to send whatever it had, and the result is green. In both cases the only trace was an ERROR line in jmeter.log that starts “Problem in JSR223 script” and names the element (ours was called “post throws”). Search the log for “Problem in JSR223 script” after every run, or catch exceptions in the script and fail the sample yourself. For assertions without code, the guide to assertions in JMeter covers the built-in ones.

Why is the first JSR223 sample so slow?

The first time a cached Groovy script runs, JMeter compiles it, and classes the script uses are loaded on first touch. That time lands inside the first sample. In our run the first sample of a script using JsonSlurper took 930 ms on each of 3 threads that started together, and a one-line script after it took 65 to 84 ms.

The JsonSlurper script was the first Groovy script in the plan, so its time also includes starting the Groovy engine. Every later run but one took 0 or 1 ms; the one outlier took 18 ms.

In a short test, those first samples inflate the maximum and the high percentiles of every JSR223 label. A warm-up period that you leave out of the analysis, or a setUp Thread Group that runs each script once, keeps them out of the numbers you report.

The compile time also skews any timing your script takes from the clock. A PostProcessor on a WebSocket read that computed delay as System.currentTimeMillis() minus a timestamp in the message reported 508 ms in one run and 678 to 679 ms in another on the first read of each thread, although the sample’s own timestamps showed the message had arrived 2 ms after it was sent. Using prev.getEndTime() instead gave 0 to 2 ms on every read. The WebSocket load testing guide shows that script in full.

Do JSR223 scripts run on LoadFocus?

Yes. LoadFocus runs uploaded JMeter test plans from 25 cloud regions, and its JMeter page lists JSR223 samplers among the elements that run as they do on your machine. Scripts typed into the elements travel inside the .jmx file, so they need nothing extra. Script files are the exception, because the upload accepts only certain file types.

The LoadFocus upload takes .jmx and .csv files, plus .txt files, certificates and images. A .groovy file is not among them, so a plan that loads .groovy script files will not find them in the cloud. Keep JSR223 scripts inline with “Cache compiled script if available” ticked. That is also the setting the speed table above favours, and it makes the .jmx self-contained for JMeter load testing on LoadFocus.

Frequently Asked Questions

What is a JSR223 Sampler in JMeter?

A JSR223 Sampler is a JMeter sampler whose work is done by a script in Groovy or another JSR223 language. The script’s return value becomes the response body, and the script can set the response code, message and success status itself. It is used for custom protocol calls, test data setup and computed values.

Should I use Groovy or BeanShell in JMeter?

Use Groovy. The JMeter manual recommends it for JSR223 elements, and with caching on it compiles once and runs faster than anything else we tried: 86 microseconds per run against 137 for the BeanShell Sampler and 779 for JavaScript. The manual also notes the limited maintenance of the BeanShell library, which does not support Java 5 syntax such as generics.

How do I get and set variables in a JMeter Groovy script?

Read a variable with vars.get(“name”) and set one with vars.put(“name”, “value”), using strings for both. For numbers, lists or other objects, use vars.putObject and vars.getObject. Variables belong to one thread; to share a value between threads, use props.get and props.put instead.

Why does my cached JSR223 script return the same value on every iteration?

The script text contains a JMeter variable or function reference, such as ${user}. With caching on, JMeter compiles the script with the first value and reuses it. Read the value inside the script with vars.get(“user”) or pass ${user} in the Parameters field, which JMeter resolves on every run.

Does an error in a JSR223 PostProcessor fail the sample?

No. In our test, an exception thrown by a JSR223 PostProcessor or PreProcessor left the sample marked as passed; the error appeared only in jmeter.log. An exception thrown by a sampler or assertion script did fail the sample. To fail a sample from a PostProcessor, call prev.setSuccessful(false) and set a response message.

Sources

  • JSR223 Sampler, PreProcessor, PostProcessor and Assertion, the variables they set up, compilation caching and the warning about variables in cached scripts: Apache JMeter User’s Manual, Component Reference, jmeter.apache.org/usermanual/component_reference.html, read 9 November 2026.
  • Sharing variables between threads with properties: Apache JMeter User’s Manual, Best Practices, section 16.13, jmeter.apache.org/usermanual/best-practices.html, read 9 November 2026.
  • LoadFocus JMeter runs: loadfocus.com/jmeter-load-testing, read 9 November 2026; the file types the upload accepts are from the LoadFocus app’s upload form.
  • Test runs: Apache JMeter 5.6.3 (official release, SHA-512 verified) with its bundled Groovy 3.0.20, on Java 11.0.19 in non-GUI mode, with a local Node.js 23.11.1 server for the HTTP and WebSocket samples, on an Apple M1 Max, 9 November 2026. Times are per run of a small script on one thread and show relative cost, not benchmarks.

Bogdan

Founder at LoadFocus

Bogdan builds and runs the tools this blog is about. He writes from what the products actually do in production, including the parts that break.

Weekly tips right in your inbox.

Don’t miss out on the latest testing tips and tricks, tools and news related to software testing.

Already +20,000 members in the list.

The Load Testing Platform with AI That Explains Your Results

Load test websites and APIs from 25 cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.

Cloud load testing, website speed and API monitoring in one place.

Hi there! Can I share a cool thing I'm working on with you?

A software testing newsletter that sparks joy.

Our goal is to create helpful content for software developers and testers.

If you're not into it, you can unsubscribe instantly.