Pre-Processors and Post-Processors

9 min read

Pre-processors and post-processors are the hooks that run immediately before and after a sampler. They give you programmatic control over what goes into a request and what gets extracted from the response — turning a sequence of static HTTP calls into a dynamic, stateful scenario that mirrors how real users actually interact with an application.

The execution sequence

Every JMeter sampler runs inside a fixed pipeline:

Step 1 of 7

Config Elements apply

HTTP Request Defaults, Cookie Manager, Header Manager, and CSV Data Set Config all initialise for this iteration. Variables are populated, cookies are loaded, defaults are set.

Pre-Processors

Pre-Processors run immediately before their parent sampler sends the request. They can modify JMeter variables, manipulate the request directly, or execute arbitrary logic.

JSR223 Pre-Processor — the most powerful option. Runs a Groovy script before the request:

// Generate a signed payload before each request
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.util.Base64
 
def payload = vars.get("requestBody")
def secret  = props.get("hmacSecret")
def mac     = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secret.bytes, "HmacSHA256"))
def sig = Base64.encoder.encodeToString(mac.doFinal(payload.bytes))
 
vars.put("signature", sig)

The sampler then sends ${signature} as a header value — computed fresh for each request.

User Parameters — defines per-thread parameter values. Each thread gets its own row of values without needing a CSV file. Useful for small, fixed per-thread data sets (3–10 users defined inline rather than in a file).

HTTP URL Re-writing Modifier — rewrites URLs to append session IDs for applications that use URL-based sessions (rather than cookies). Extracts the session ID from the previous response and appends it to the current request URL automatically.

BeanShell Pre-Processor — the legacy scripting option. Functionally equivalent to JSR223 Pre-Processor with Groovy, but slower. Use JSR223 instead.

Post-Processors

Post-Processors run immediately after the sampler receives its response. Their primary purpose is extraction — pulling values from the response body, headers, or status and storing them as JMeter variables.

JSON Extractor

The most-used post-processor for REST APIs. Evaluates a JSONPath expression against the response body and stores the result in a named variable.

Configuration:

  • Names of variables: authToken (comma-separated for multiple)
  • JSON Path expressions: $.token (semicolon-separated for multiple)
  • Match No.: 1 (first match), 0 (random match), -1 (all matches as array)
  • Default Values: NOT_FOUND (used if the path matches nothing)
Login Request (POST /api/auth/login)
└── JSON Extractor
      Names: authToken, userId, refreshToken
      Paths: $.token; $.user.id; $.refresh_token

After this runs, ${authToken}, ${userId}, and ${refreshToken} are available to every sampler that executes after it in the same thread.

Regular Expression Extractor

Extracts text using a Java regular expression. More flexible than JSON Extractor but harder to read for structured data.

Configuration:

  • Reference Name: csrfToken
  • Regular Expression: name="_csrf" value="(.+?)"
  • Template: $1$ (first capture group)
  • Match No.: 1
  • Default Value: NO_CSRF_TOKEN

The capture group (...) in the regex is what gets extracted. $1$ in the template means "use the first capture group". Use $0$ for the entire match.

Boundary Extractor

Extracts text between two fixed string boundaries. Faster than regex for simple cases where the boundaries are plain text:

  • Left Boundary: "session_id": "
  • Right Boundary: "

Captures the session ID without writing a regex. Fragile if the surrounding format changes, but readable and fast.

XPath Extractor

Evaluates an XPath expression against an XML response and stores the result. Used for SOAP services and XML APIs.

JSR223 Post-Processor

Full Groovy access to the response object and variable map:

import groovy.json.JsonSlurper
 
def body    = prev.getResponseDataAsString()
def json    = new JsonSlurper().parseText(body)
def items   = json.items
 
// Store count for assertion
vars.put("itemCount", items.size().toString())
 
// Store first item details
vars.put("firstItemId",    items[0].id.toString())
vars.put("firstItemPrice", items[0].price.toString())

prev is the SampleResult from the sampler that just ran. prev.getResponseDataAsString() returns the response body as a string.

The canonical pattern: login → extract → reuse

Nearly every authenticated load test follows this structure:

Thread Group
├── Once Only Controller
│   ├── POST /api/auth/login
│   │   └── JSON Extractor → authToken, userId
│   └── HTTP Header Manager: Authorization: Bearer ${authToken}
│       (at Thread Group level, so all subsequent requests carry the token)
└── Loop Controller (10 loops)
    ├── GET /api/products
    ├── POST /api/cart/add → JSON Extractor → cartId
    └── POST /api/checkout
          Body: {"cartId": "${cartId}", "userId": "${userId}"}

The login runs once per thread (Once Only Controller). The JSON Extractor captures the token and user ID. All subsequent requests carry the token automatically via the Header Manager.

⚠️ Common mistakes

  • Placing a Post-Processor at Thread Group level instead of as a child of the specific sampler. A JSON Extractor at Thread Group level runs after every sampler in the group — not just the login. It will attempt to extract $.token from a product listing response (which has no token field) and write NOT_FOUND to authToken, breaking all subsequent authenticated requests. Always attach extractors as direct children of the sampler they should read from.
  • Using the Default Value NOT_FOUND without checking it. If extraction fails silently, ${authToken} becomes the string NOT_FOUND and every subsequent Authorization: Bearer NOT_FOUND returns 401. Add a Response Assertion to the login sampler checking the response contains token so the test fails loudly on extraction failure rather than continuing with a broken state.
  • Forgetting Match No. semantics. Match No. 1 returns the first match. 0 returns a random match from all matches. -1 stores all matches as varName_1, varName_2, etc., and varName_matchNr contains the count. For APIs that return an array of items and you want the first ID, use $.items[0].id with Match No. 1 — not Match No. 0 which would pick a random item.

🎯 Practice task

Build an authenticated flow using extractors.

  1. Add a POST request to your test plan targeting test.k6.io/auth/token/login/ with body {"username":"test_case","password":"1234!"}. This endpoint returns {"access":"<token>","refresh":"<token>"}.
  2. Add a JSON Extractor as a child of the login request: variable name accessToken, JSON path $.access, default NOT_FOUND.
  3. Add an HTTP Header Manager at Thread Group level with Authorization: Bearer ${accessToken}.
  4. Add a second request: GET test.k6.io/my/crocodiles/. This endpoint requires authentication.
  5. Run with 1 user, 1 loop. In View Results Tree: confirm the login response contains access, and confirm the second request's Request tab shows Authorization: Bearer <actual token> — not Bearer NOT_FOUND.
  6. Add a Response Assertion to the login sampler asserting the body contains access. Change the login URL to something invalid and re-run — confirm the assertion fails and the test reports an error rather than silently running with NOT_FOUND.

// tip to track lessons you complete and pick up where you left off across devices.