Scripting with JSR223 and Groovy for Advanced Test Logic: Configuration, Design Patterns, and Trade-Offs
The best JMeter script is often the one you can delete. Built-in components expose intent in the tree, inherit JMeter scope semantics, require less custom testing, and usually reduce generator risk. The decision is not “code versus no code”; it is which state and behavior should remain declarative and which small irreducible transformation deserves executable code.
Learning objectives
- Choose built-in components/functions before JSR223 where semantics are equivalent.
- Choose inline cached snippets or script files based on size/reuse/testability.
- Choose vars or props based on ownership/lifetime—not convenience.
- Prefer immutable/local state over mutable global objects.
- Choose cached Groovy over BeanShell/JavaScript hot paths.
- Recognize when a growing Groovy subsystem should become a tested Java library/plugin instead.
1. Mandatory examples remain local/free
127.0.0.1:8019, ≤2
threads, ≤600 samples/variant.
No managed load cloud, enterprise secret manager, shared performance
environment, Kubernetes, paid CI, external API, or third-party
plugin is required.
2. Built-in component/function versus JSR223
| Built-in component/function | JSR223 + Groovy |
|---|---|
| Prefer for extraction, assertions, counters, timers, CSV, controllers, HTTP configuration, common functions. | Use for a transformation/validation/state operation that cannot be expressed clearly/declaratively. |
| Intent visible in tree and standard docs. | Intent hidden in source unless carefully named/documented. |
| Lower custom test/maintenance surface. | Requires code review, unit tests, exception/logging/performance discipline. |
| Usually easier across CI/remote engines. | Script files must exist at correct user.dir/path on every engine. |
Do not replace a JSON JMESPath Extractor with
JsonSlurper just because Groovy can parse JSON. The
built-in component already models that state transition.
3. JMeter function versus JSR223 element
__groovy can be useful for a tiny expression embedded
in a field. It has cache-safe bindings such as vars and
props. However, once the expression becomes multi-line,
reusable, needs unit tests, or has exception branches, prefer a
named JSR223 element/script file.
Functions can also obscure execution timing because they are evaluated when the containing field is resolved. A visible PreProcessor/PostProcessor often makes lifecycle clearer.
4. Inline script versus script file
| Inline cached script | Script file |
|---|---|
| Good for a very small one-off helper. | Better for reusable/multi-line logic and separate code review/unit testing. |
| Check Cache compiled script if available. | JMeter can compile/cache script files when engine supports it. |
| Avoid ${VAR}/function expansion in source; use bindings. | File contents receive JSR223 bindings at runtime; relative path resolves from user.dir. |
| Lives inside JMX, easy single-artifact portability. | Every local/CI/remote engine must receive the file at the expected path. |
5. vars versus props
| vars | props |
|---|---|
| Per-thread virtual-user state. | Process-wide JMeter properties. |
| Correct for tokens, extracted IDs, canonicalized input, user counters. | Correct for immutable run/environment knobs and values intentionally shared within one engine. |
| Safe from other JMeter threads unless you deliberately share objects elsewhere. | All threads can read/write; mutable use needs real synchronization/design. |
| Not shared across threads/engines. | Not distributed shared memory; each remote engine is a separate JVM/property set. |
Rule: if you find yourself using
props.put('CURRENT_USER_TOKEN', ...), stop and
re-evaluate ownership.
7. Cached Groovy versus slower scripting engines
JMeter best practices recommend a JSR223 language implementing
Compilable for intensive load; Groovy does. BeanShell
and JavaScript are not recommended for intensive JSR223 paths in
current JMeter guidance.
Do not assume a JavaScript engine is present on every modern JDK/JMeter runtime. If your existing plan relies on one, pin/provision the engine explicitly and benchmark it—or migrate small hot-path logic to cached Groovy/built-ins.
8. Groovy helper versus custom Java library/plugin
A 10–50 line helper can stay Groovy. When scripts grow into a shared domain library with many classes, complex parsing, large data structures, frequent allocation, concurrency primitives, or public APIs across dozens of plans, consider moving logic into a separately tested/versioned Java library. A real custom JMeter TestElement/plugin is justified only when new component behavior/UI/lifecycle integration is needed.
That choice adds build/release/classpath/plugin compatibility work. Do not create a plugin merely to avoid learning built-in JMeter components.
9. Cache-size trade-off
The default compiled script cache holds 100 compiled scripts. A normal plan should not need hundreds of dynamically different script bodies. If cache pressure appears because code embeds changing data into script source, fix the source/binding design first rather than increasing the cache.
Only tune jsr223.compiled_scripts_cache_size when
evidence shows a legitimate large number of stable scripts and
generator memory/cache behavior has been measured.
10. Logging design
Use INFO for setup/version/run summaries and rare lifecycle events, DEBUG for targeted temporary investigation, ERROR with contextual identifiers for genuine failures. Avoid logging full response bodies, tokens, passwords, environment variables, or one INFO line per sample in a hot loop.
Per-sample logs add formatting, locking/appender/file I/O and can
enlarge jmeter.log enough to affect the generator.
11. Exception strategy
Pure helper functions should reject invalid inputs explicitly. Do
not catch Exception and continue with stale variables.
If a script cannot produce valid state, surface the
exception/failure and let the plan's defined error policy act.
Catch only when you can add useful context or transform the error into a deliberate domain assertion without losing the original cause.
12. Configuration-layer boundaries
| Layer | Examples | Do not confuse with |
|---|---|---|
| JMeter core/test plan | JSR223 scope, built-in extractors/assertions, cache checkbox, vars/props | Groovy/JVM engine internals or SUT latency. |
| Java/JVM | Groovy classes, heap/GC, JIT, classloading | Target service CPU. |
| OS/network | process CPU, file I/O, DNS/sockets | Script semantic correctness. |
| SUT | HTTP processing/service timing | Generator Groovy overhead. |
| Plugin/library | extra ScriptEngine/JAR/custom Java TestElement | JMeter core Groovy support. |
| CI/container | script-file mounts/user.dir, JVM/JAR versions, runner CPU | Local test-plan scope. |
13. Worked scenario
Requirement: “A response has five JSON fields and one proprietary checksum transformation.”
- JSON JMESPath Extractors/Assertion handle the five fields/status.
- One cached Groovy helper computes only the proprietary checksum from thread-local inputs.
- The checksum helper has separate unit tests.
- The plan records generator CPU/throughput before/after introducing it.
- If the checksum library grows or becomes shared across many plans, move it to a tested Java library rather than growing ad-hoc Groovy indefinitely.
14. Decision table
| Requirement | Preferred approach | Reason/evidence |
|---|---|---|
| Extract JSON field | JSON JMESPath Extractor | Visible, standard, no custom code. |
| Simple per-user count | Counter / vars | Thread ownership explicit. |
| Tiny custom normalization | Cached JSR223 PreProcessor | Irreducible transformation, local vars. |
| Reusable 40-line helper | Script file + unit tests | Review/test/cache/CI deployment easier. |
| Process-wide immutable run ID | -J property / props read |
Shared configuration, no per-sample mutation. |
| Complex reusable domain library | Tested Java library/plugin if justified | Stronger structure/performance/versioning than growing scripts. |
| Hot-path BeanShell/JS | Refactor to built-ins or cached Groovy | Current JMeter guidance favors Compilable engine. |
15. Configured versus achieved load and evidence
Configured load is thread count, loops, timers, target endpoint and
intended sample count. Achieved load is completed valid
samples/second after Groovy compilation/execution, generator CPU/GC,
result/logging overhead, and target service time. Preserve script
source/hash, cache/config note, vars/props ownership table,
unit-test output, JTL + matching jmeter.log, generator
CPU/heap, target event counts/service time, and experimental
limitations.
Knowledge check
When should JSON parsing remain built-in?
When JMESPath/JSON assertions can express the needed extraction/validation clearly; scripting would only duplicate standard behavior.
What is the main portability cost of Script File?
Every execution environment/remote engine must have the file at the path resolved from user.dir.
Why isn't props a distributed shared-memory store?
Each JMeter engine is a separate JVM with its own Properties object.
What should you fix before increasing the compiled-script cache?
Changing/dynamic script source—especially runtime data embedded in source—so stable code can actually reuse cache entries.
When should Groovy become Java code?
When custom logic grows into a complex shared library/performance-critical subsystem whose testing/versioning/concurrency needs exceed a small script helper.
Official references and version notes
- JMeter Component Reference — JSR223 Sampler — compilation caching, bindings, script-file paths, SampleResult behavior, and Groovy guidance.
- JMeter Component Reference — JSR223 PreProcessor — scoped pre-sample scripting and bindings.
- JMeter Component Reference — JSR223 PostProcessor — post-sample scripting and previous-sample access.
- JMeter Component Reference — JSR223 Assertion — scripted assertion scope and bindings.
- JMeter Functions — __groovy — Groovy function bindings and cache-safe variable access.
-
JMeter Properties Reference
—
jsr223.compiled_scripts_cache_sizeand advanced Groovy/JSR223 configuration. - JMeter Best Practices — JSR223/Groovy recommendations, compiled scripting, lean results, and non-GUI load execution.
- Apache JMeter downloads — current stable release and Java requirement.
- Apache JMeter issue #6402 — JMeter 5.6.3 stack traces identify bundled Groovy 3.0.20 and document newer-JDK compatibility context.
Version-sensitive statements were rechecked against current Apache
JMeter primary documentation on 2026-09-05. The course baseline
remains Apache JMeter 5.6.3 with a
Java 17 JDK; JMeter 5.6.3 requires Java 8+. The
JMeter 5.6.3 binary line uses Groovy 3.0.20.
Groovy's JSR223 engine implements Compilable. JMeter
recommends script files (compiled/cached when supported) or inline
script text with
Cache compiled script if available checked. The
compiled-script cache defaults to
jsr223.compiled_scripts_cache_size=100. Do not put
changing ${VAR} or JMeter function replacement
directly inside cached script text: JMeter expands it before the
script reaches the engine, so the first replacement can be
captured by the cache. Read runtime state through
vars, props,
Parameters/args, or other supplied
bindings instead. JSR223 Sampler exposes log,
Label, FileName,
Parameters, args,
SampleResult, sampler, ctx,
vars, props, and OUT;
assertions/processors expose the appropriate
current/previous-sample bindings. JMeter explicitly recommends
migration from BeanShell to JSR223 + Groovy for
performance/support, and Groovy is preferred over non-Compilable
scripting engines for intensive load paths.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.