The basics will handle 80% of your correlation needs. But some scenarios require more advanced techniques. Let me share the tricks I have picked up over years of dealing with tricky application architectures.
Sometimes the value you need is not in a single response. You need to follow a chain. For example: the login returns a session cookie, the session cookie unlocks a page that contains a CSRF token, and the CSRF token is needed for the actual API call. Each extraction feeds the next.
Thread Group
├── GET / # Homepage
│ └── Regex Extractor: session_init_token # From Set-Cookie header
│ Field to check: Response Headers
│ Regex: INIT_TOKEN=(.+?);
│
├── POST /api/init-session # Initialize session
│ Header: Cookie: INIT_TOKEN=${session_init_token}
│ └── JSON Extractor: session_id # Server confirms session
│ JSONPath: $.sessionId
│
├── GET /dashboard # Dashboard page (needs session)
│ Header: X-Session: ${session_id}
│ └── CSS Extractor: api_nonce # API nonce embedded in page
│ Selector: script[data-nonce]
│ Attribute: data-nonce
│
└── POST /api/transfer # The actual test action
Header: X-Session: ${session_id}
Header: X-Nonce: ${api_nonce}
Body: {"amount": 1000, "to": "ACC-002"}Not everything lives in the response body. Authentication tokens, rate limit info, pagination cursors, and request IDs often come in response headers. For the Regular Expression Extractor, change "Field to check" from "Body" to "Response Headers". For other extractors, you may need Groovy.
// Extract custom headers that other extractors cannot handle easily
def headers = prev.getResponseHeaders()
// Extract X-Request-Id for tracing
def requestIdMatcher = (headers =~ /X-Request-Id:\s*(.+)/)
if (requestIdMatcher.find()) {
vars.put("request_id", requestIdMatcher.group(1).trim())
} else {
vars.put("request_id", "NOT_FOUND")
}
// Extract rate limit info
def limitMatcher = (headers =~ /X-RateLimit-Remaining:\s*(\d+)/)
if (limitMatcher.find()) {
def remaining = limitMatcher.group(1).trim() as int
vars.put("rate_limit_remaining", remaining.toString())
if (remaining < 10) {
log.warn("Rate limit almost exhausted: ${remaining} requests remaining")
}
}
// Extract pagination cursor from Link header
def linkMatcher = (headers =~ /Link:.*<(.+?)>;\s*rel="next"/)
if (linkMatcher.find()) {
vars.put("next_page_url", linkMatcher.group(1).trim())
} else {
vars.put("next_page_url", "NO_MORE_PAGES")
}Some applications return dynamic values in redirect URLs. For example, after payment processing, the server redirects to /payment/callback?transactionId=TXN-789&status=success. To capture the transaction ID, you need to either disable "Follow Redirects" and extract from the Location header, or enable redirect following and extract from the final URL.
# Method 1: Disable "Follow Redirects" in the HTTP Request
# The 302 response has a Location header:
# Location: /payment/callback?transactionId=TXN-789&status=success
# Regex Extractor on the 302 response:
# Field to check: Response Headers
# Regex: transactionId=(.+?)&
# Variable: txn_id
# Then add a separate HTTP Request to follow the redirect manually:
# GET /payment/callback?transactionId=${txn_id}&status=success
# Method 2: Keep "Follow Redirects" enabled
# Check "Apply to: Sub-samples" in the Regex Extractor
# Or extract from the URL of the final redirected page:
# Field to check: URL
# Regex: transactionId=(.+?)(&|$)
# This searches the final URL after all redirectsWhen you extract multiple values (Match No. = -1), the ForEach Controller lets you iterate through all of them. This is common for scenarios like "add all items from search results to cart" or "process all pending orders."
# Step 1: Extract all product IDs from search results
# GET /api/search?q=laptop
# JSON Extractor:
# Variable: product_id
# JSONPath: $.results[*].id
# Match No.: -1
# Creates: product_id_1=P001, product_id_2=P002, product_id_3=P003, product_id_matchNr=3
# Step 2: ForEach Controller
# Input Variable Prefix: product_id
# Start Index: 0 (JMeter adds 1 internally, so starts with product_id_1)
# End Index: (leave blank for all)
# Output Variable: current_product_id
# Add "_" before number: checked
# Step 3: Inside ForEach, use ${current_product_id}
# GET /api/products/${current_product_id}
# POST /api/cart {"productId":"${current_product_id}"}
# This loops 3 times:
# Iteration 1: current_product_id = P001
# Iteration 2: current_product_id = P002
# Iteration 3: current_product_id = P003Be careful with ForEach Controller in load tests. If the search returns 50 results and you iterate through all 50, each virtual user generates 50x more requests than expected. Your 100-user test suddenly produces 5000 concurrent API calls. Either limit the extraction (use Match No. = 1 or a filter) or account for the multiplication in your test design.
JMeter variables (set with vars.put()) are thread-local. Thread 1 cannot see Thread 2's variables. But sometimes Thread 1 creates something (like an order) that Thread 2 needs to act on (like approve the order). For this, you need JMeter properties, which are global.
// Thread Group 1 -- "Order Creator"
// JSR223 PostProcessor after creating an order:
def orderId = vars.get("order_id")
// __setProperty is a JMeter function, but in Groovy:
props.put("shared_order_id", orderId)
log.info("Order ${orderId} created and shared via properties")
// Thread Group 2 -- "Order Approver"
// JSR223 PreProcessor before the approval request:
def orderId = props.get("shared_order_id")
if (orderId != null) {
vars.put("order_to_approve", orderId)
log.info("Picked up order ${orderId} for approval")
} else {
log.warn("No order available for approval yet")
// Optionally wait and retry
}
// *** WARNING: This is a basic approach. For production-grade
// cross-thread coordination, use a shared queue (LinkedBlockingQueue)
// stored in props to handle multiple orders.For complex cross-thread scenarios (like producer-consumer patterns), store a java.util.concurrent.LinkedBlockingQueue in JMeter properties. Thread Group 1 puts items into the queue, Thread Group 2 takes items from it. This is thread-safe and handles timing naturally.
Key Point: Advanced correlation includes chaining extractions across requests, extracting from headers and redirects, iterating with ForEach Controller, and sharing data across threads using JMeter properties.