Scripting with JSR223 and Groovy for Advanced Test Logic: Diagnostics, Failure Modes, and Production Practices
Script failures are especially dangerous because a bad script can corrupt both the workload and its evidence: stale variables can alter requests, global objects can cross-contaminate users, log storms can saturate disk, swallowed exceptions can silently continue, and SampleResult mutation can erase genuine target failures. Diagnose the script as generator code while preserving the target's original result.
Learning objectives
- Diagnose cached direct variable substitution and repair it with bindings.
- Identify non-thread-safe process-global objects.
- Measure and eliminate per-sample logging overhead.
- Reject BeanShell/JavaScript hot paths without a measured reason.
- Preserve exceptions instead of swallowing them.
- Prevent PostProcessors/Assertions from rewriting genuine target failures to success.
1. Preserve first-failure evidence
127.0.0.1:8019, ≤2 threads.
Preserve JMX, script source/file, cache checkbox/property, input
row, vars/props ownership, JTL, jmeter.log, generator
CPU/heap/log size, target event log/stats, exact CLI and versions
before correction. Never substitute a production target or delete
the failed evidence.
2. Diagnostic sequence
Scripting sits inside the existing JMeter execution model. It receives scoped JMeter state, performs one controlled calculation or mutation, and returns control to the test plan; it does not replace workload, protocol, target, result, or safety semantics.
flowchart TD E[Preserve JTL + jmeter.log + script + target evidence] --> V[Confirm JMeter / Java / Groovy / plugin/tool versions] V --> C[Confirm exact JMX/data/properties/CLI + authorized target] C --> S[Inspect JSR223 element scope / cache / source / parameters] S --> D[Inspect vars vs props vs shared objects + resolved values] D --> P[Inspect protocol/session/sample state and exception] P --> G[Inspect generator CPU/GC/log/file/classpath] G --> T[Inspect SUT event count/service timing] T --> X[Inspect remote/CI/container user.dir/JAR/file state if relevant] X --> F[Least destructive correction] F --> R[Small controlled rerun and compare]
3. Intentionally broken example: ${RAW_NAME} inside
cached script
Cache compiled script is checked:
// BROKEN ON PURPOSE when cache is enabled:
def raw = '${RAW_NAME}'
vars.put('REQUEST_NAME', raw.trim().toLowerCase(java.util.Locale.ROOT))
JMeter expands ${RAW_NAME} before compilation. The
first expanded script can be cached, so later CSV rows can keep
producing the first canonical value. Target events show repeated
one-name traffic even though CSV advanced.
Repair:
// Cache-safe: script text stays constant; runtime value is read from JMeterVariables.
def raw = vars.get('RAW_NAME')
vars.put('REQUEST_NAME', raw.trim().toLowerCase(java.util.Locale.ROOT))
Script source is now stable and the current thread's value is read at execution time. Preserve the broken JTL/log/events before rerunning 1 thread ×4 rows.
4. Failure mode: non-thread-safe global object in props
Broken concept:
if (props.get('SEEN_NAMES') == null) {
props.put('SEEN_NAMES', new ArrayList())
}
props.get('SEEN_NAMES').add(vars.get('REQUEST_NAME'))
Properties being shared does not make the stored
ArrayList thread-safe, and the lazy initialization
itself races. Worse, per-user collection does not belong in global
test-plan state at all.
Repair: keep per-user data in vars. For real
cross-thread aggregation, prefer JMeter results/backend telemetry or
an explicitly designed concurrent structure with bounded memory and
documented lifecycle—not ad-hoc properties.
5. Failure mode: INFO logging per sample
Broken:
log.info("user=${vars.get('REQUEST_NAME')} body=${prev.getResponseDataAsString()}")
This formats data and writes to jmeter.log for every
sample, possibly including sensitive response data. Under load it
can become disk/lock/GC pressure.
Repair: log one setup summary, use assertions/JTL fields/target telemetry for routine evidence, and temporarily enable narrowly scoped DEBUG only for a small reproduction.
6. Failure mode: BeanShell or JavaScript in a hot path without reason
JMeter explicitly recommends migration from BeanShell to JSR223 +
Groovy for performance and newer Java support. Groovy implements
Compilable; BeanShell/JavaScript do not provide the
same compiled-cache path in JMeter guidance.
Do not mechanically translate a script if a built-in JMeter component can replace it. Migration order: built-in first → cached Groovy for irreducible logic → benchmark again.
7. Failure mode: swallowing exceptions and continuing with stale state
Broken:
try {
vars.put('REQUEST_NAME', canonicalize(vars.get('RAW_NAME')))
} catch (Exception ignored) {
// continue; REQUEST_NAME may still contain a previous iteration's value
}
The HTTP sampler can now send stale data while the script appears
quiet. Repair by validating inputs and allowing the
exception/failure to surface, or set an explicit sentinel and fail
with an assertion. Preserve the original stack trace in
jmeter.log.
8. Failure mode: mutating SampleResult to hide a real failure
Broken PostProcessor:
if (!prev.isSuccessful()) {
prev.setSuccessful(true)
prev.setResponseCode('200')
prev.setResponseMessage('forced success')
}
This destroys the original HTTP/error taxonomy. A performance gate can pass while the target is returning 500/401/assertion failures.
Repair: never force real target samples to success. If an error is expected by scenario design, preserve the original status and classify it with a dedicated label/assertion/analysis rule.
9. Failure mode: Script File works in GUI but fails in CI
Relative JSR223 Script File paths resolve from the Java
user.dir system property. Running JMeter from another
working directory can make
scripts/canonicalize.groovy disappear.
Repair: define a reproducible project root and run CLI from it, or supply an explicit project-root property/path policy. In distributed mode, deploy the file to every engine at the same expected path; controller-local existence is insufficient.
10. Failure mode: dynamic source defeats cache
A script embeds current timestamps, IDs, or variable replacement into source text, creating many unique MD5 cache keys. Compilation/cache churn consumes CPU/memory and can evict legitimate scripts from the default 100-entry cache.
Keep source constant and pass data through bindings/Parameters. Do not “fix” dynamic source by blindly increasing cache size.
11. Failure mode: deep ctx/sampler manipulation when a component exists
ctx/sampler are powerful, but
reflective/deep manipulation tightly couples the plan to JMeter
implementation details. If HTTP Header Manager, Counter, extractor,
assertion, controller, or property field already provides the
behavior, use the component.
12. Causal performance table
| Symptom | Script/generator cause | Target cause to distinguish | Evidence |
|---|---|---|---|
| Throughput drops, HTTP p95 stable | compile/script/log overhead | target service slowdown | overall JTL duration + generator CPU + target service_wall_ms. |
| Wrong repeated user value | cached ${VAR} source | server caching bug | CSV row + vars + target name distribution. |
| Intermittent cross-user data | global mutable object/property race | server session mix-up | thread vars + properties/object ownership + target events. |
| jmeter.log explodes | per-sample logging/stack trace loop | server log growth | JMeter log bytes + generator disk/CPU. |
| CI-only script failure | user.dir/script/JAR mismatch | target regression | CI path/classpath + no/unchanged target traffic. |
| Green JTL despite target errors | SampleResult forced success | target actually healthy | raw target status/server events versus mutated JTL. |
13. Security-sensitive scripting
Scripts can read environment variables, filesystem files, JVM properties, credentials, process state, databases/messages/APIs, and can start processes. Treat Groovy as code with the same trust as JMeter itself. Use fake values locally; never dump environment variables/secrets to logs; do not invoke shell/processes from a hot path; keep recorder CA, RMI, CI secrets, containers and OS/JVM tuning outside this lab.
14. Troubleshooting shortcuts to reject
- Do not add blanket retries or arbitrary long sleeps around script exceptions.
- Do not increase heap to compensate for unbounded global objects/logs.
- Do not disable all listeners without evidence; isolate the actual scripting/log/result cost.
- Do not use global properties as per-user state or synchronization hacks.
- Do not disable TLS/RMI verification.
- Do not experiment on production/public targets.
- Do not increase threads while script correctness/cache/thread-safety is unresolved.
-
Do not delete failed JTL/
jmeter.logor target evidence.
Knowledge check
What target symptom points to cached ${VAR} substitution?
Changing CSV rows produce repeated first-value requests while the target itself is simply receiving what JMeter sends.
Why is an ArrayList stored in props unsafe?
props is shared across threads, but the ArrayList and initialization/mutation are not automatically thread-safe or semantically appropriate.
Why can per-sample INFO logging lower achieved load?
Formatting, synchronization, file I/O and larger jmeter.log consume generator CPU/disk/GC resources.
What is wrong with catching every exception and continuing?
Downstream samples can use stale/partial variables, turning a clear script failure into silent workload corruption.
How can JTL be green while the target failed?
A script can improperly mutate prev/SampleResult success/code/message; compare preserved target evidence and remove that mutation.
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.