Correlation — Extracting and Reusing Dynamic Values

9 min read

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:

  1. Repeated long strings that appear in both a response body and a subsequent request — these are likely session IDs or tokens.
  2. Numeric IDs in URLs like /orders/48291/ that were not in your test data CSV — the server generated them.
  3. Headers like X-CSRF-Token or Set-Cookie on responses followed by those values in subsequent request headers.
  4. Form fields with hidden type in HTML responses — almost always correlation candidates.

For each identified dynamic value:

  1. Find the response that first contains it.
  2. Add an extractor (JSON, Regex, or Boundary) to that sampler.
  3. Find every subsequent request that uses the hardcoded value.
  4. 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 sends Authorization: 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 intend Match No. 1 (first). When a JSONPath expression matches multiple nodes — for example $.items[*].id matching an array of item IDs — Match No. 0 picks a random one each iteration. Match No. 1 always picks the first. Match No. -1 stores 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.

  1. Use the test.k6.io API. First request: POST /auth/token/login/ with {"username":"test_case","password":"1234!"}. Extract access into ${accessToken}.
  2. Second request: POST /my/crocodiles/ with Authorization: Bearer ${accessToken} and body {"name":"Grogg","sex":"M","date_of_birth":"2015-01-01"}. Extract the new crocodile's id field into ${crocodileId}.
  3. 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}.
  4. 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 accessToken contains an actual JWT, not NOT_FOUND.
  5. 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 propagating NOT_FOUND downstream.

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