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
$.tokenfrom a product listing response (which has notokenfield) and writeNOT_FOUNDtoauthToken, breaking all subsequent authenticated requests. Always attach extractors as direct children of the sampler they should read from. - Using the Default Value
NOT_FOUNDwithout checking it. If extraction fails silently,${authToken}becomes the stringNOT_FOUNDand every subsequentAuthorization: Bearer NOT_FOUNDreturns 401. Add a Response Assertion to the login sampler checking the response containstokenso the test fails loudly on extraction failure rather than continuing with a broken state. - Forgetting Match No. semantics. Match No.
1returns the first match.0returns a random match from all matches.-1stores all matches asvarName_1,varName_2, etc., andvarName_matchNrcontains the count. For APIs that return an array of items and you want the first ID, use$.items[0].idwith Match No.1— not Match No.0which would pick a random item.
🎯 Practice task
Build an authenticated flow using extractors.
- 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>"}. - Add a JSON Extractor as a child of the login request: variable name
accessToken, JSON path$.access, defaultNOT_FOUND. - Add an HTTP Header Manager at Thread Group level with
Authorization: Bearer ${accessToken}. - Add a second request: GET
test.k6.io/my/crocodiles/. This endpoint requires authentication. - Run with 1 user, 1 loop. In View Results Tree: confirm the login response contains
access, and confirm the second request's Request tab showsAuthorization: Bearer <actual token>— notBearer NOT_FOUND. - 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 withNOT_FOUND.