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/s scales with the VUs. 1.0/s → 4.9/s → 9.8/s. Each VU contributes ~1 iteration/s (because of the sleep(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 duration and * default: 10 looping VUs for 30s — k6 tells you how it set up the run from your options: one scenario running the default function with 10 VUs "looping" (looping VUs) for 30 s. There's the VU model described by k6 itself: VUs that loop over default.

The checks (lesson 4).

  • ✓ status is 200 and ✓ price is 7500 — the names you put in your check, 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 through default. 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=10 and vus_max...: 10 — the VUs: 10 concurrent users, and they stayed at 10 the whole time (flat load, min=max=10). Here vus is 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 the avg (2.1 ms) can fool you while the max (110 ms) and the p(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 the sleep(1). Contrast it with http_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. The default ✓ [===] 10 VUs 30s is 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 the default function (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.post to /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, and http_reqs would be ~600 with iterations at 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 vus line (and vus_max): vus: 10 min=10 max=10 → 10 concurrent users.
  • (b) In the checks line: checks: 100.00% ✓ 600 ✗ 0 → 100% of the criteria passed.
  • (c) In the http_reqs line: http_reqs: 300 9.97/s → 300 calls in total.
  • (d) In the max of the http_req_duration line: 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.