Module 2: The K6 Script And Virtual Users
7. VUs vs iterations and reading the k6 summary
Overview
We reach the misunderstanding we raised in lesson 1 and have been circling: a VU isn't an iteration, and neither one is a request. They're three concepts that sound similar, are counted differently, and confusing them makes you misread a whole load test. A VU is a concurrent user (an actor); an iteration is a complete lap through the script (a finished function); a request is an HTTP call (an action within the lap). Ten VUs, in thirty seconds, with sleep(1), produce around three hundred iterations —not ten, not thirty—. Counting that chain right is knowing how to read a load test.
The other half of the lesson is practical: reading the k6 summary. When you run k6 run, at the end it prints a block of text full of metrics, and that's where these three numbers —VUs, iterations, requests— appear together, alongside the checks. You'll learn to locate each one in the summary and to read what they say. We focus on the blocks this module covers —checks, iterations, http_reqs, vus, vus_max—; the http_req_duration block (the latencies and their percentiles) you'll see in the summary and know it exists, but its dissection is module 3.
Connection to the module: this lesson brings everything together. The VUs-vs-iterations arithmetic uses lessons 2 (the loop), 5 (the think time), and 6 (vus/duration); the summary gathers the checks from lesson 4 and everything else. The k6 summary is shown as labeled content —real and correct, verified against k6's documentation, but not run on this machine—; the arithmetic we confirm with real runs of the Python generator (1, 5, and 10 VUs). Everything labeled as Python output was measured in this environment with Python 3.14.0. With this lesson you close the anatomy of the script and the VU model; lesson 8 assembles it in a mini-project.
The gym: machines, sets, and reps
Think of it this way. In a gym there are 10 machines for the same exercise. Each person who arrives uses a machine, does their set, rests a moment, and repeats —one set after another— for the hour their routine lasts. At the end of the day, the manager counts three different things: how many machines were occupied at once (10), how many sets were completed in total (hundreds, adding up everyone's over the hour), and how many individual reps were done (thousands, if each set is several reps). No one would confuse "10 machines" with "300 sets" with "3000 reps": they're three measures of three things.
In k6 it's identical. The VUs are the machines: how many concurrent users there are at once (10). The iterations are the sets: how many complete laps through the script were done in total, adding up all the VUs over the whole duration (hundreds). The requests (http_reqs) are the reps: how many HTTP calls were fired in total (if each iteration makes one request, iterations and requests coincide; if it makes three, there are triple the requests). Three numbers, three things. The VU is the concurrency; the iteration, the completed work; the request, the individual action.
VUs, iterations, and requests are three different things. VUs = concurrent users (the gym machines). Iterations = complete laps through the script, adding up all VUs (the sets). Requests = individual HTTP calls (the reps). 10 VUs aren't 10 iterations or 10 requests: they produce hundreds of each over time.
The arithmetic, confirmed with real runs
The relationship between the three numbers isn't mysterious; it's a formula you can estimate in advance. With a script that makes one request per lap and a sleep(t), each VU completes approximately 1 / (t + request_time) iterations per second. Since the request time is tiny next to the sleep, with sleep(1) each VU does ~1 iteration per second, and the total is:
iterations ≈ vus × duration / (think_time + request_time)
≈ vus × duration (when think_time dominates, e.g. sleep(1))
Let's confirm it with three real runs of the generator, all with sleep(1), scaling up. First 1 VU / 5 s:
What to expect. 1 × 5 = ~5 iterations. Real output:
vus............: 1
iterations.....: 5 (1.0/s)
checks.........: 100.00% (10 of 10)
Now 5 VUs / 4 s:
What to expect. 5 × 4 = ~20 iterations. Real output:
vus............: 5
iterations.....: 20 (4.9/s)
checks.........: 100.00% (40 of 40)
And 10 VUs / 10 s:
What to expect. 10 × 10 = ~100 iterations. Real output:
vus............: 10
iterations.....: 100 (9.8/s)
checks.........: 100.00% (200 of 200)
Read them together, because they take apart the misunderstanding in one blow:
- VUs ≠ iterations. In no case is the number of iterations equal to the number of VUs. 1 VU → 5 iterations; 5 VUs → 20; 10 VUs → 100. The VU is who; the iteration is how many laps it did.
- The formula holds. 1×5=5, 5×4=20, 10×10=100. With
sleep(1), iterations ≈vus × duration. Predictable. - The checks are double the iterations. 5 iterations → 10 checks; 20 → 40; 100 → 200. Because each iteration does 2 checks (status and price, lesson 4). Requests, iterations, and checks are three different counts linked by the script: 1 request per iteration, 2 checks per iteration.
iterations/sscales with the VUs. 1.0/s → 4.9/s → 9.8/s. Each VU contributes ~1 iteration/s (because of thesleep(1)), so the total pace is ~vus/s. Double the VUs and you double the pace.
With this, you should never again read "vus: 10" and write down "10 requests." Ten VUs are ten actors that, over the duration, complete hundreds of laps and fire hundreds of requests.
The k6 summary, read line by line (content)
Now the other half: how all this looks in the k6 summary. When you run k6 run quote_test.js with the 10 VUs / 30 s and sleep(1) script we built in the module, k6 prints a block like this at the end. It's shown as reference content —correct and verified against k6's documentation, but k6 isn't installed in this environment, so we don't run it here—:
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: quote_test.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 30s max duration (incl. graceful stop):
* default: 10 looping VUs for 30s (gracefulStop: 30s)
✓ status is 200
✓ price is 7500
checks.........................: 100.00% ✓ 600 ✗ 0
data_received..................: 61 kB 2.0 kB/s
data_sent......................: 59 kB 2.0 kB/s
http_req_blocked...............: avg=9µs min=1µs med=3µs max=1.2ms p(90)=6µs p(95)=10µs
http_req_connecting............: avg=2µs min=0s med=0s max=0.5ms p(90)=0s p(95)=0s
http_req_duration..............: avg=2.1ms min=0.5ms med=1.7ms max=110ms p(90)=3.2ms p(95)=4.6ms
{ expected_response:true }...: avg=2.1ms min=0.5ms med=1.7ms max=110ms p(90)=3.2ms p(95)=4.6ms
http_req_failed................: 0.00% ✓ 0 ✗ 300
http_req_receiving.............: avg=40µs min=10µs med=30µs max=0.9ms p(90)=70µs p(95)=95µs
http_req_sending...............: avg=20µs min=5µs med=14µs max=0.5ms p(90)=35µs p(95)=50µs
http_req_waiting...............: avg=2.0ms min=0.5ms med=1.6ms max=109ms p(90)=3.1ms p(95)=4.4ms
http_reqs......................: 300 9.97/s
iteration_duration.............: avg=1.0s min=1.0s med=1.0s max=1.11s p(90)=1.0s p(95)=1.0s
iterations.....................: 300 9.97/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
running (0m30.1s), 00/10 VUs, 300 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs 30s
It's a lot of text, but only a few lines matter to us in this module. Let's go through them:
The header (what ran).
scenarios: ... 1 scenario, 10 max VUs, 30s max durationand* default: 10 looping VUs for 30s— k6 tells you how it set up the run from youroptions: one scenario running thedefaultfunction with 10 VUs "looping" (looping VUs) for 30 s. There's the VU model described by k6 itself: VUs that loop overdefault.
The checks (lesson 4).
✓ status is 200and✓ price is 7500— the names you put in yourcheck, each with its mark. The✓means that criterion always passed.checks.........: 100.00% ✓ 600 ✗ 0— the total count. 600 checks, all passed, none failed. Why 600? Because there were 300 iterations × 2 checks per iteration = 600. You recognize the pattern from the Python runs (checks = 2 × iterations).
The three figures of the misunderstanding.
http_reqs......: 300 9.97/s— the requests: 300 HTTP calls in total, at ~10 per second. Since the script makes one request per iteration, it coincides with the iterations.iterations.....: 300 9.97/s— the iterations: 300 complete laps throughdefault. Notice: it's not 10 (the VUs) or 30 (the seconds). It's ~vus × duration= 10 × 30 = 300, the same formula you confirmed in Python.vus............: 10 min=10 max=10andvus_max...: 10— the VUs: 10 concurrent users, and they stayed at 10 the whole time (flat load,min=max=10). Herevusis the concurrency, distinct from the 300 iterations. The three figures —10, 300, 300— are three things.
What exists but is module 3's.
http_req_duration..: avg=2.1ms ... p(90)=3.2ms p(95)=4.6ms— the latency of the requests, with its average, minimum, median, maximum, and percentiles. This is the block module 3 studies: why theavg(2.1 ms) can fool you while themax(110 ms) and thep(95)tell another story. Here just locate it: it's the request time, and the percentiles are M3.http_req_failed..: 0.00% ✓ 0 ✗ 300— the error rate of the requests (0% failed). Also M3.iteration_duration..: avg=1.0s— how long each complete lap lasted: ~1 second, almost all of it because of thesleep(1). Contrast it withhttp_req_duration(2.1 ms): the iteration lasts a thousand times longer than the request because it includes the think time. That difference —request vs iteration— is the one from lesson 5, now visible in the summary.
The footer (the progress).
running (0m30.1s), 00/10 VUs, 300 complete and 0 interrupted iterations— the progress summary: it ran 30.1 s, ended with 0 active VUs, and completed 300 iterations without interrupting any. Thedefault ✓ [===] 10 VUs 30sis the progress bar at the end.
If you take five lines from this whole block, let them be these: checks (did it respond well?), http_reqs (how many calls?), iterations (how many laps?), vus (how many concurrent?), and —saved for M3— http_req_duration (how fast?). With those five you read 90% of any load test.
Why the Python summary resembles the k6 one (on purpose)
You'll have noticed that the Python generator's summary imitates the shape of k6's: a block of vus, iterations, checks. It's deliberate. The generator models the same world —looping VUs, iterations, checks— and reports the same figures, so the bridge between "what k6 would do" and "what we measured" is direct. The differences are honest: the Python summary doesn't compute percentiles or separate http_req_waiting from http_req_sending (that's k6 machinery, and the metrics in depth are M3); it only reports what this module needs —how many VUs, how many iterations, how many checks passed—. When in module 3 the generator starts computing p95 and RPS with statistics, its summary will get even closer to k6's. For now, the columns they share —vus, iterations, checks— already let you read both with the same eyes.
Common mistakes
Reading vus: 10 as "10 requests" or "10 iterations." What happens: someone reports "I ran 10 users" and assumes there were 10 calls to the API. Why it happens: they confuse concurrency with volume. How to detect it: their counts don't match the summary's http_reqs or iterations, which are much larger. How to fix it: vus is how many run at once; iterations and http_reqs are how much work they did in total over the duration. Ten VUs / 30 s with sleep(1) ≈ 300 iterations, not 10.
Confusing iterations with http_reqs. What happens: someone sees both say 300 and thinks they're always equal. Why it happens: in the example the script makes one request per iteration, so they coincide. How to detect it: in a script that makes several requests per lap (quoting and booking, for example), http_reqs is larger than iterations. How to fix it: remember that iterations counts laps through the script and http_reqs counts HTTP calls; their ratio is "requests per iteration." They coincide only when there's exactly one request per lap.
Focusing on the avg of http_req_duration and ignoring the max and the p(95). What happens: someone reads avg=2.1ms, concludes "very fast," and doesn't see the max=110ms. Why it happens: the average is the most visible and most reassuring. How to detect it: the avg looks good but the max or the p(95) are much larger —a sign that some requests were slow—. How to fix it: in this module it's enough to locate the http_req_duration block; reading it in depth —why the average lies and the p95 rules— is module 3. But do notice the pattern: a low avg with a high max hides a tail of slow requests.
Exercises
Exercise 1 — The three numbers. In the lesson's k6 summary, vus: 10, iterations: 300, and http_reqs: 300 appear. (a) What does each one measure, in one sentence? (b) Why is iterations 300 and not 10? (c) Why does http_reqs coincide with iterations in this case, and when would it not coincide?
See solution
- (a)
vus= concurrent virtual users (how many run at once);iterations= complete laps through thedefaultfunction (how much work was completed in total);http_reqs= HTTP calls fired in total. - (b) Because the 10 VUs run 30 seconds with
sleep(1), and each VU does ~1 iteration per second: 10 × 30 ≈ 300 iterations. The VUs are 10; the laps they did among all of them are 300. - (c) It coincides because the script makes one request (
http.postto/quote) per iteration: 300 iterations × 1 request = 300 requests. It would not coincide if each iteration made several requests —for example, quoting and booking would be 2 requests per lap, andhttp_reqswould be ~600 withiterationsat 300—.
Exercise 2 — Predict and read. You're going to run a script with sleep(1) and options = { vus: 20, duration: '10s' }, with 1 request and 2 checks per iteration. (a) How many iterations do you expect? (b) How many requests (http_reqs)? (c) How many total checks?
See solution
- (a) ~
vus × duration= 20 × 10 = ~200 iterations. - (b) One request per iteration → ~200 requests (
http_reqs). - (c) Two checks per iteration → 200 × 2 = ~400 total checks. If they all pass, the summary would say
checks: 100.00% ✓ 400 ✗ 0.
(The real 20 VUs / 10 s run you'll see in lesson 8's mini-project, with a 500 ms think time —so the iterations will be more than 200, because the pace is faster with less pause—.)
Exercise 3 — Locate in the summary. A colleague ran a test and wants to answer four questions by looking at the k6 summary. In which line do they find each answer? (a) "How many concurrent users were there?" (b) "What percentage of the responses was correct?" (c) "How many calls to the API were made in total?" (d) "What was the slowest request?"
See solution
- (a) In the
vusline (andvus_max):vus: 10 min=10 max=10→ 10 concurrent users. - (b) In the
checksline:checks: 100.00% ✓ 600 ✗ 0→ 100% of the criteria passed. - (c) In the
http_reqsline:http_reqs: 300 9.97/s→ 300 calls in total. - (d) In the
maxof thehttp_req_durationline:max=110ms→ the slowest request took 110 ms. (Reading this line well —average vs maximum vs percentiles— is module 3; here you just locate it.)
Summary and next step
In this lesson you took apart the central misunderstanding of k6: VUs, iterations, and requests are three different things. VUs are concurrent users (the gym machines), iterations are complete laps through the script adding up all VUs (the sets), and requests are individual HTTP calls (the reps). You confirmed it with real runs —1 VU → 5 iterations, 5 → 20, 10 → 100— that confirm the formula iterations ≈ vus × duration (with sleep(1)) and that the checks are 2 × iterations. And you learned to read the k6 summary (as content): to locate checks, http_reqs, iterations, vus, and vus_max, and to recognize that the http_req_duration block —the latencies and their percentiles— exists but is module 3's material.
Before moving on you should be able to: explain the difference between a VU, an iteration, and a request without hesitation; estimate the three figures of a flat load from vus, duration, and the think time; and find in a k6 summary the lines for checks, iterations, requests, and VUs.
With this you close the anatomy of the script and the VU model: you know what each piece does and how to read what they produce. Lesson 8 is the mini-project: you'll write a complete k6 script for /quote (content, with its summary) and its executable equivalent in Python —N VUs with ThreadPoolExecutor, status and price check, iteration and error counting— and run it for real against the canonical API, closing the module with the two faces in your hands.
Resources
- k6's execution model — VUs and iterations — the reference that distinguishes virtual users from iterations. The foundation of this lesson's arithmetic.
- k6 built-in metrics — the list of all the summary's metrics (
checks,http_reqs,iterations,vus,http_req_duration...) and what each one measures. The legend of the block you read. - The end-of-test summary — how the block k6 prints on finishing is composed. The source of the summary shown as content.
statistics.quantiles— Python documentation — the percentiles the generator doesn't yet compute and that module 3 will use to bring its summary closer to k6's. The bridge toward the metrics in depth.