Correlation is the technique of capturing dynamic values that a server generates during a test session and feeding them back into subsequent requests. It is the single most important concept for making recorded or replayed tests actually work — and the most common reason why "the test works once but fails on the second run."
The problem that correlation solves
When you record a user session — either with JMeter's proxy recorder or a browser extension — the recording captures exact request and response values. The recorded request might look like this:
POST /api/checkout
Content-Type: application/json
{
"cartId": "a3f9c812-7e44-4b1c-a6d5-f23087b51234",
"csrfToken": "eyJhbGciOiJIUzI1NiJ9.abc123",
"orderId": "ORD-20260508-00042"
}
Every value in that body was generated by the server during that specific recording session. When you replay the test, the server generates new values:
cartId:b7d2e991-3f11-4c8e-b7e0-a14096c67890(different cart)csrfToken:eyJhbGciOiJIUzI1NiJ9.xyz789(new CSRF token — the old one is invalid)orderId: the server never accepts a client-provided order ID — it generates its own
Without correlation, the replayed request sends the old, stale values. The server rejects them: 400 Bad Request, 403 Forbidden, or a silent processing error. The test appears to "run" but never executes the actual user flow.
Values that always need correlation
Step-by-step correlation example
Consider a checkout flow: search for a product, add it to cart, proceed to checkout.
Step 1 — Login and extract the auth token:
POST /api/auth/login
Body: {"email":"${email}","password":"${password}"}
└── JSON Extractor
Variable: authToken
Path: $.access_token
Default: NOT_FOUND
Step 2 — Create a cart and extract the cart ID:
POST /api/cart/create
Authorization: Bearer ${authToken}
└── JSON Extractor
Variable: cartId
Path: $.cart.id
Default: NOT_FOUND
Step 3 — Add a product and capture the CSRF token from the response header:
POST /api/cart/${cartId}/items
Authorization: Bearer ${authToken}
└── Regular Expression Extractor
Reference Name: csrfToken
Regular Expression: X-CSRF-Token: (.+)
Template: $1$
Field to check: Response Headers
Step 4 — Checkout using all extracted values:
POST /api/checkout
Authorization: Bearer ${authToken}
X-CSRF-Token: ${csrfToken}
Body: {"cartId":"${cartId}","productId":${productId}}
└── JSON Extractor
Variable: orderId
Path: $.order.id
Each step extracts what the next step needs. The variables form a chain — break any extraction and the chain collapses downstream.
Extracting from HTML forms (CSRF tokens)
Many web applications embed CSRF tokens in HTML form elements:
<input type="hidden" name="_csrf" value="eyJhbGciOiJIUzI1NiJ9.abc">To extract this from a page response, use a Regular Expression Extractor or Boundary Extractor on the sampler that loads the page.
Boundary Extractor (simpler):
- Left Boundary:
name="_csrf" value=" - Right Boundary:
" - Variable:
csrfToken
Regular Expression Extractor (more flexible):
- Regular Expression:
name="_csrf" value="([^"]+)" - Template:
$1$ - Variable:
csrfToken
The sampler that loads the form page must come before the sampler that submits the form, so the extraction has already run by submission time.
Extracting multiple values from one response
JSON Extractor supports multiple extractions in a single element using semicolons:
Names of variables: authToken;userId;userRole;sessionId
JSON Path expressions: $.token;$.user.id;$.user.role;$.session.id
Match No.: 1;1;1;1
Default Values: NOT_FOUND;NOT_FOUND;NOT_FOUND;NOT_FOUND
Four variables extracted from one JSON response, one Post-Processor element. The semicolon delimiter keeps the configuration compact and the extraction logic visible in one place.
Recording and identifying what needs correlation
JMeter's proxy recorder (HTTP(S) Test Script Recorder) captures all HTTP traffic as a sequence of samplers. After recording, go through the samplers and look for:
- Repeated long strings that appear in both a response body and a subsequent request — these are likely session IDs or tokens.
- Numeric IDs in URLs like
/orders/48291/that were not in your test data CSV — the server generated them. - Headers like
X-CSRF-TokenorSet-Cookieon responses followed by those values in subsequent request headers. - Form fields with
hiddentype in HTML responses — almost always correlation candidates.
For each identified dynamic value:
- Find the response that first contains it.
- Add an extractor (JSON, Regex, or Boundary) to that sampler.
- Find every subsequent request that uses the hardcoded value.
- Replace the hardcoded value with
${variableName}.
Verifying extraction worked
Add a Debug Sampler (right-click → Add → Sampler → Debug Sampler) after the extractors. It logs all current JMeter variables to the View Results Tree. Use it to confirm ${authToken} contains an actual token, not NOT_FOUND, before adding more requests that depend on it. Disable the Debug Sampler before running load tests.
⚠️ Common mistakes
- Not validating extraction before building the full flow. Add extractors one at a time and run with 1 user after each addition, checking the Debug Sampler output. Building a 20-step test plan and then discovering the first extractor was wrong means all 20 downstream requests are broken — and debugging which extractor failed is painful.
- Hardcoding extracted values for "testing the test." If extraction fails and returns
NOT_FOUND, the next request sendsAuthorization: Bearer NOT_FOUND— which the server rejects with 401. Without a Response Assertion on the login sampler checking the token field exists, the test continues running a broken flow and reports misleading 401 errors rather than a clear extraction failure. - Using
Match No. 0(random) when you intendMatch No. 1(first). When a JSONPath expression matches multiple nodes — for example$.items[*].idmatching an array of item IDs —Match No. 0picks a random one each iteration.Match No. 1always picks the first.Match No. -1stores all of them as numbered variables. Each has valid use cases; picking the wrong one produces non-deterministic test behaviour.
🎯 Practice task
Build a correlated multi-step flow.
- Use the
test.k6.ioAPI. First request: POST/auth/token/login/with{"username":"test_case","password":"1234!"}. Extractaccessinto${accessToken}. - Second request: POST
/my/crocodiles/withAuthorization: Bearer ${accessToken}and body{"name":"Grogg","sex":"M","date_of_birth":"2015-01-01"}. Extract the new crocodile'sidfield into${crocodileId}. - Third request: GET
/my/crocodiles/${crocodileId}/. Confirm in View Results Tree that the URL contains the actual numeric ID that was returned in step 2, not the string${crocodileId}. - Add a Debug Sampler after the login step. Run with 1 user. In View Results Tree, click the Debug Sampler result and read the variable list — confirm
accessTokencontains an actual JWT, notNOT_FOUND. - Delete the Debug Sampler. Add a Response Assertion to the login request asserting the body contains
access. Break the login URL (change to/auth/token/invalid/). Re-run and confirm the test fails loudly at the assertion rather than silently propagatingNOT_FOUNDdownstream.