skip to content

A JMeter JSR223 PreProcessor signs each request from an inline script, yet every request after the first carries the same signature. Why?

level: seniorimportance: should knowfreq 42%

answer

  1. Two features that are fine apart
  2. Substitution runs every time, compilation does not
  3. The cache key is computed once
  4. First request's values, forever

basics

~10 s

The inline script contains ${...} references and compilation caching is on. JMeter compiles the first substituted version of the text and keeps reusing it, so every later execution re-runs the first request's values.

solid answer

~50 s

JMeter substitutes `${...}` into a JSR223 element's inline **Script** text on every execution, but with **Cache compiled script if available** ticked the element compiles that text once and caches the result. The digest it looks the cache up by is computed the first time and never recomputed, so the compiled artefact keeps the first substitution baked into it — the nonce, timestamp or payload that was current on the first pass. The manual warns about exactly this: *"ensure your script code does not use JMeter variables or JMeter function calls directly in script code as caching would only cache first replacement."* The repair is not to untick the box but to stop putting substituted syntax in the script: read the values through the objects JMeter binds into scope, so the script text is constant and safe to compile once.

code

groovy · 13 lines
groovy
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

// WRONG: JMeter substitutes ${nonce} into the source, then the compiled
// script freezes whatever the first execution happened to produce.
def payload = "${nonce}" + vars.get("body")

// RIGHT: the script text never changes, so compiling it once is safe.
def payload = vars.get("nonce") + vars.get("body")

Mac mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(props.get("signing.key").getBytes("UTF-8"), "HmacSHA256"))
vars.put("signature", mac.doFinal(payload.getBytes("UTF-8")).encodeHex().toString())

go deeper

for a junior

Recognise the shape: a value that should change per request never changes. You are not expected to reach the caching explanation unaided at this level.

for a middle

Explain that JMeter substitutes into the inline script every time but compiles it once, so a cached script carries the first substitution forever.

for a senior

Diagnose it from the symptom, prove it by toggling the cache, and then fix the cause by taking the reference out of the script text rather than by disabling caching.

for a principal

Turn the incident into a standard: constant script text, values through bindings or parameters, and a review rule that catches ${} inside a cached script before it ships.

## The two mechanisms that collide On its own, each half is reasonable: 1. **Substitution.** The inline `Script` is an element property. Before every execution JMeter prepares the element's properties, which resolves `${nonce}`, `${__time()}` and any other reference into plain text. That much really does happen every time. 2. **Compilation caching.** With the checkbox on and a `Compilable` engine, the element compiles the script text and stores the `CompiledScript` in a process-wide cache keyed by the MD5 digest of that text. The collision is in the second half. The element computes its digest **once** and holds on to it; on later executions it hands the same stale key to the cache and gets back the same compiled script. The freshly substituted text is produced and then thrown away. So the signing script that read `"${nonce}"` when the first request went out is still, on request five thousand, the script that was compiled from request one's nonce. ## Why the symptom looks like a signing bug Everything downstream behaves consistently, which is what makes it slow to diagnose: - the PreProcessor runs every time and reports no error; - the variable it writes is set every time; - the signature is a well-formed, correctly computed HMAC — of stale input; - the server rejects requests with a replay or signature error, pointing suspicion at the crypto rather than at JMeter. The giveaway is that the *first* request of each thread succeeds and everything after it fails identically. ## Confirming it in about a minute - Untick **Cache compiled script if available** on that element and re-run. If the signatures start varying, caching was the cause. - Or log the value the script computed at the top of the script and compare consecutive iterations; identical values from an input that should change is the same evidence. Neither of these is the fix — they are the confirmation. ## The three ways out, best first 1. **Take the reference out of the script text.** Replace `"${nonce}"` with a lookup against the bound context so the script text is byte-identical on every execution. The compiled artefact is then genuinely reusable and caching costs you nothing. 2. **Pass the value through `Parameters`.** That field is a property, so JMeter substitutes it, and its text reaches the script as a binding without ever becoming part of the compiled source. 3. **Untick the checkbox.** This works and it is the wrong reflex: you keep the fragile pattern and pay for compilation on every execution instead. Reserve it for a script you cannot change right now. ## The rule worth writing on the wall **A cached script's text must be a constant.** Anything that varies enters through the bindings, not through `${}`. Stated that way the rule also covers the cases people miss — a `${__counter()}` call, a `${__time()}` stamp, a `${__P(...)}` property read — all of which are substituted into the source and all of which freeze the same way. A script file sidesteps the whole problem, because a file's contents are never substituted in the first place and its cache key is its path and last-modified time rather than its content.

  • Would moving that JMeter signing script into a Script File fix it, and why?
    Yes. A script file's contents are never passed through JMeter's variable replacement, so there is no substitution to freeze; the placeholder problem is replaced by the ordinary requirement to read values through the bound context. The file's cache key is its language, absolute path and last-modified time, so editing the file also invalidates the cached compilation.
  • Why is unticking 'Cache compiled script if available' the wrong first fix for this JMeter element?
    It treats the symptom. The script keeps its `${...}` reference, so it must now be recompiled on every single execution to stay correct, and the next engineer who ticks the box for speed reintroduces the bug silently. Making the script text constant fixes the cause and lets caching stay on.
  • Does this JMeter caching problem also bite ${__time()} and ${__counter()} calls inside a script?
    Yes. Function calls in the inline script are substituted exactly like variable references, so a cached script keeps the first execution's timestamp or counter value in its compiled form. Anything that is meant to change per execution has to arrive through a binding rather than through the script's own text.

It is like filling in a paper form once and then photocopying it for every customer. The copier is fast and faithful, and it reproduces the first customer's answers forever.

saying these in an interview costs you the question

  • Blames the HMAC implementation instead of the plan
  • Thinks JMeter stops substituting once a script is compiled
  • Believes the cached script is refreshed each iteration
  • Assumes the PreProcessor stopped running because no error appeared
  • Treats unticking the cache box as the permanent fix