Module 6: Checks Groups And Realistic Scenarios
6. Correlation: extract and reuse (quote → book)
Overview
We reach the heart of the module. Until now, although the scenario has several steps and the data varies, each step is independent: you fix the quote data, you fix the booking data, you fix the confirm data. But a real flow doesn't work like that. In a real flow, each step uses what the previous one returned. You quote a room and the API responds with a price; when you book, you send that price, not one you made up. The booking returns an identifier; when you confirm, you use that identifier, not any one. The steps are chained by the data that flows between them. Reproducing that chain in a load test is correlation, and it's what separates a real scenario from a list of isolated requests.
Correlation is a technical word for something simple: extract a value from one response and use it in the next request. You quote → extract the price_cents from the response → send it in the booking. You book → extract the booking_id from the response → use it in the URL to confirm. You don't make up the value or hardcode it: you take it from what the API just returned. And here's the deep part: a correlated value is dynamic —it changes in each iteration, because each booking generates a new booking_id—. If you tried to hardcode the id, it would work for one iteration and fail in all the others. Correlation is the only way to chain steps when the link between them is a value the server generates on the fly.
Connection to the module: correlation is the fourth and central piece of the realistic scenario. It brings together everything before: the checks (lesson 2) verify each step, the groups (lesson 4) organize them, the parametrization (lesson 5) varies the initial data, and the correlation chains them. The quote → book → confirm flow is actually run in Python against the canonical API, with the price_cents and the booking_id really correlated; the k6 pattern —res.json('booking_id') and using it in the next request— goes as content. You'll see the complete chain measured in this environment with Python 3.14.0, including the proof that the extracted booking_id is the one that works in the next step.
The chain of custody
Think of it as a chain of custody, the one used with a piece of evidence or a package that passes from hand to hand. At a building's reception, a package arrives and the guard puts a label on it with a tracking number they generate right then —it didn't exist before—. When the package goes up to the 5th floor, the courier doesn't make up a number: they use the one the guard put. When the recipient signs, they sign against that same number. Each link of the chain uses the identifier the previous link passed it; the number was born in the first step and travels intact to the end. If someone in the middle used a made-up number, the chain would break: the system wouldn't find that package.
A quote → book → confirm flow is that chain of custody. The quote generates a datum (the price_cents) that the booking uses. The booking generates an identifier (the booking_id) that the confirmation uses —a number that is born in the booking step and didn't exist before—. Correlation is respecting the chain: at each step, use the value the previous step passed you, not a made-up one. And since the booking_id is born at runtime (each booking generates a new one), the only way to have it is to extract it from the response —just as the courier can only read the number the guard just wrote, not guess it—.
Correlation is extracting a value from one response and using it in the next request, instead of making it up or hardcoding it. It's the scenario's chain of custody: the
price_centsyou were quoted travels to the booking; thebooking_idthe booking generated travels to the confirmation. Since that id is born at runtime and is different in each iteration, extracting it from the response is the only way to chain the flow.
Correlation in k6 (content)
In k6, extracting a value from a response is reading its body with res.json('key'); using it in the next step is putting it in the body or the URL of the next request. Here's the three-step flow with the two correlations —the price and the id— marked:
// scenario_correlated.js - quote -> book -> confirm with correlation.
// SHOWN AS CONTENT: k6 is not installed in this environment.
import http from 'k6/http';
import { check, group, sleep } from 'k6';
const BASE_URL = 'http://localhost:8000';
const params = { headers: { 'Content-Type': 'application/json' } };
export default function () {
// --- Step 1: quote --- (produces price_cents)
let price;
group('quote', function () {
const body = JSON.stringify({ room: 'Studio', tier: 'pro', hours: 4 });
const res = http.post(`${BASE_URL}/quote`, body, params);
check(res, {
'status is 200': (r) => r.status === 200,
'price is correct': (r) => r.json('price_cents') === 12800,
});
price = res.json('price_cents'); // EXTRACTS the price from the response
});
sleep(1);
// --- Step 2: book --- (uses price_cents; produces booking_id)
let bookingId;
group('book', function () {
// CORRELATION 1: the quoted price_cents travels to the booking.
const body = JSON.stringify({ room: 'Studio', tier: 'pro', hours: 4, price_cents: price });
const res = http.post(`${BASE_URL}/book`, body, params);
check(res, {
'status is 200': (r) => r.status === 200,
'booking is confirmed': (r) => r.json('confirmed') === true,
'booking_id present': (r) => String(r.json('booking_id')).startsWith('bk-'),
});
bookingId = res.json('booking_id'); // EXTRACTS the id from the response
});
sleep(1);
// --- Step 3: confirm --- (uses booking_id)
group('confirm', function () {
// CORRELATION 2: the generated booking_id travels to the URL.
const res = http.get(`${BASE_URL}/booking/${bookingId}`);
check(res, {
'status is 200': (r) => r.status === 200,
'id matches': (r) => r.json('booking_id') === bookingId,
'price matches quote': (r) => r.json('price_cents') === price,
});
});
}
The two correlations, marked:
price = res.json('price_cents')(end of step 1) →price_cents: price(step 2's body). The price the quote returned is extracted and reused in the booking. On a real API, resending that price would let the backend verify the quote is still valid and respond409if it changed; our lab server doesn't validate it (the booking just confirms), but the pattern of reusing the quoted price —instead of recomputing or making it up— is the same. In k6, thepricevariable holds the value between groups.bookingId = res.json('booking_id')(end of step 2) →`${BASE_URL}/booking/${bookingId}`(step 3's URL). The id the booking generated is extracted and put into the confirmation's URL. This is the link you couldn't hardcode: each booking produces a differentbooking_id.- The
'price matches quote'check of step 3 closes the chain: it verifies the price the confirmation returns is the same one quoted at the beginning. If any link broke, this check would catch it.
Notice that the variables (price, bookingId) are declared outside the group()s so the value survives from one step to the next. That "holding between steps" is the physical mechanism of correlation: a value that comes out of one response and waits until the next request.
The complete flow, run in Python
In Python, extracting is reading a key from the body (body.get("price_cents"), b2.get("booking_id")); reusing is putting that value into the next request. The generator runs the three-step flow with the two real correlations and checks at each step:
# The core of the correlated scenario (one iteration of a VU).
# --- Step 1: quote --> extract price ---
s1, b1 = post_json(f"{BASE_URL}/quote", {"room": room, "tier": tier, "hours": hours})
price = b1.get("price_cents") # EXTRACTS the price
# --- Step 2: book (uses price) --> extract booking_id ---
# CORRELATION 1: the price travels in /book's body
s2, b2 = post_json(f"{BASE_URL}/book",
{"room": room, "tier": tier, "hours": hours, "price_cents": price})
booking_id = b2.get("booking_id") # EXTRACTS the id
# --- Step 3: confirm (uses booking_id) ---
# CORRELATION 2: the booking_id travels in GET /booking/<id>'s URL
s3, b3 = get_json(f"{BASE_URL}/booking/{booking_id}")
# step 3 checks that close the chain:
record("confirm: id matches", b3.get("booking_id") == booking_id)
record("confirm: price matches quote", b3.get("price_cents") == price)
The parametrized data (lesson 5) picks room, tier, hours at the start; from there, price and booking_id flow from one response to the next. Let's run the healthy scenario, 4 VUs / 3 s / think 100 ms, with checks at each step.
What to expect. If the correlation works, the three steps must pass their checks at 100%: the price adds up, the booking is confirmed with a real booking_id, and the confirmation finds that id with that price. Real output in this environment:
----------------------------------------------------------------
Scenario quote->book->confirm -> 4 VUs / 3s / think 100ms
----------------------------------------------------------------
vus............: 4
iterations.....: 109 (35.2/s)
checks.........: 100.00% (872 of 872)
quote: status is 200..............: 109 ok / 0 fail
quote: price is correct...........: 109 ok / 0 fail
book: status is 200...............: 109 ok / 0 fail
book: confirmed is true...........: 109 ok / 0 fail
book: booking_id present..........: 109 ok / 0 fail
confirm: status is 200............: 109 ok / 0 fail
confirm: id matches...............: 109 ok / 0 fail
confirm: price matches quote......: 109 ok / 0 fail
http_errors....: 0
per group (avg latency, n calls):
group 'quote '.....: n=109 avg=1.06ms
group 'book '.....: n=109 avg=0.54ms
group 'confirm'.....: n=109 avg=0.47ms
Read it as the chain of custody working:
- 8 checks per iteration, all 8 green (872 = 109 × 8). Two from the quote step, three from the booking, three from the confirmation. The complete chain held in the 109 iterations.
confirm: id matches: 109 ok / 0 fail— this is the check that proves the id correlation. The confirmation requestedGET /booking/<booking_id>with the id the booking returned, and the API found exactly that booking and returned that same id. If the scenario had made up the id, this check would have failed 109 times (404, id not found). The 109 ok is the proof that the extractedbooking_idis the real one.confirm: price matches quote: 109 ok / 0 fail— this closes the whole chain. The price the confirmation returns is the same one quoted three steps back. Theprice_centstraveled from the quote to the booking and was stored in the booking; the confirmation reads it and it matches. The three custody links, intact.- The per-group breakdown confirms that each step ran 109 times (one per iteration), with the quote as the heaviest (lesson 4).
Declaration: for the correlation, this server generates the booking_id with a sequential counter —bk-000001, bk-000002, …—, a unique id per booking. It's different from the descriptive format bk_Focus_basic_3 the M1 canonical server uses (which would repeat the same id between bookings with the same data); here we need unique ids for the correlation to make sense. And that uniqueness is exactly what makes it interesting: each booking_id was different (bk-000001, bk-000002, …, generated on the fly by the API), and still step 3 always found its own, because it extracted it from the response instead of making it up. That's correlation: following the thread the server generates, not guessing it.
What would happen if you broke the chain
To make clear why correlation matters, imagine that in step 3 you used a made-up id instead of the extracted one —for example, a fixed bk-999999—. The chain would break:
GET /booking/bk-999999would return 404 (that booking doesn't exist; no one created it).- The
confirm: status is 200check would fail in all the iterations (404, not 200). - The
confirm: id matchescheck would fail too (there's no id to match).
And worse: the test would seem to "run" (it would complete the iterations), but it would be testing a confirmation that never works, giving you latency data of a 404 instead of a real confirmation. That's the danger of not correlating: it's not just that it fails, it's that it can fail silently in the right sense (the request is made, but against a resource that doesn't exist), and if you don't look at the checks, you don't even notice. Correlation —extracting the real id— is what makes step 3 test what you say it tests.
Common mistakes
Hardcoding the link's value instead of extracting it. What happens: someone puts a fixed booking_id (bk-000001) in the confirm step instead of using the one the booking returned. Why it happens: it seems simpler, and maybe in the first iteration it even works (if that id exists by chance). How to detect it: the confirm step fails in almost all the iterations (404), or "passes" against a resource that isn't the one this iteration created. How to fix it: extract the id from the booking's response (res.json('booking_id')) and use it. A value generated on the fly by the server is never hardcoded.
Not storing the extracted value outside the group/step. What happens: in k6, someone declares const price = res.json(...) inside the group('quote', ...), and in the booking step price no longer exists (it stayed in the group's scope). Why it happens: they forget the value has to survive from one step to the next. How to detect it: undefined in the booking's body, or a ReferenceError. How to fix it: declare the variable outside the group()s (with let price; before the first group) and assign it inside. The correlated value needs to live between steps.
Not verifying the correlation with a check. What happens: someone correlates the id but doesn't add a check confirming step 3 found it, so if the extraction fails (id undefined), the test doesn't notice. Why it happens: they trust that "if it got here, it worked." How to detect it: the test runs but you'd never know whether step 3 hits real bookings or 404s. How to fix it: add a check in the correlated step —'id matches', 'status is 200'— that fails if the id wasn't the real one. The check is what turns "the request was made" into "the request did the right thing."
Exercises
Exercise 1 — Identify the two correlations. In the quote → book → confirm flow, there are two values extracted from one response and reused in the next. (a) Name them. (b) For each, say which response it's extracted from and which request it's reused in (and whether it goes in the body or the URL).
See solution
- (a) The
price_centsand thebooking_id. - (b)
price_cents: extracted from thePOST /quoteresponse and reused in the body ofPOST /book.booking_id: extracted from thePOST /bookresponse and reused in the URL ofGET /booking/<id>.
Exercise 2 — The check that proves the correlation. In the real run, confirm: id matches gave 109 ok / 0 fail. (a) What exactly does that result prove about the correlation? (b) If the scenario had used a made-up (fixed) booking_id, what would that check have shown and why?
See solution
- (a) It proves the
booking_idstep 3 used in the URL was the real one —the one the booking generated and returned—, because the API found exactly that booking and returned the same id. The 109 matches confirm the extraction and reuse worked in every iteration, with a different id each time. - (b) It would have shown
0 ok / 109 fail(or similar). A made-up id likebk-999999corresponds to no created booking, soGET /booking/bk-999999would return 404, and bothstatus is 200andid matcheswould fail in all iterations. The check would have betrayed that the chain was broken.
Exercise 3 — Why can't the booking_id be hardcoded? Explain, with the chain-of-custody analogy, why the price_cents could in theory be precomputed but the booking_id can never be hardcoded. What differentiates the two values?
See solution
The difference is when and who generates each value.
The price_cents is deterministic: for Studio/pro/4h, it's always 12800, computed with a known rule. In theory you could precompute it (in fact, lesson 5's dataset carries it as expected). Correlating it from the response is more honest (you test the price the API actually gave), but the value is predictable.
The booking_id is generated by the server at runtime, and it's different in each booking (bk-000001, bk-000002, …). It didn't exist before the booking step created it. In the chain of custody, it's the tracking number the guard writes at the moment the package arrives: the courier can't guess it, only read it. That's why the booking_id can only be obtained by extracting it from the booking's response; hardcoding it guarantees you point to the wrong booking (or to none). A value born on the fly can never be fixed in advance.
Summary and next step
In this lesson you built the heart of the realistic scenario: correlation, extracting a value from one response and reusing it in the next request. The quote → book → confirm flow is a chain of custody: the price_cents you were quoted travels to the booking; the booking_id the booking generated travels to the confirmation's URL. You ran it for real in Python: 4 VUs / 3 s gave 872 of 872 checks (100%), and in particular confirm: id matches and confirm: price matches quote at 109 ok / 0 fail proved the extracted booking_id was the real one and that the price traveled intact through the three steps —with a different booking_id in each iteration, always found because it was extracted instead of made up—. And it became clear what happens if you break the chain (a made-up id gives cascading 404s): correlation is what makes the next step test what you say it tests.
Before moving on you should be able to: identify a flow's correlated values (what's extracted and where it's reused); write the extraction (res.json('key')) and the reuse (in body or URL); store the value between steps (outside the groups); verify the correlation with a check; and explain why an id generated on the fly is never hardcoded.
Lesson 7 puts the last touch of realism: think time with jitter. A correlated scenario like yours, running with no pause, fires the three steps at maximum speed —a hammer, not a user—. And if all the VUs pause exactly the same, they create artificial spikes (they all request at once). You'll learn to put a random pause between steps to desynchronize the VUs, and you'll see the effect measured: the same scenario at 2158 iterations/s with no pause against 13/s with realistic think time.
Resources
- Correlation and dynamic data in k6 — the official reference for how to extract a value from a response and reuse it in the next. The exact source of this lesson.
- The Response object —
res.json()ink6/http— how a value is read from the response body (res.json('booking_id')), the correlation's extraction mechanism. What makes getting the id possible. dict.get— Python built-in types — the method the generator extractsprice_centsandbooking_idfrom the body with in Python, without blowing up if the key is missing. The executed equivalent ofres.json().