Module 2: The K6 Script And Virtual Users
6. `options`: `vus` and `duration`
Overview
You already have the complete script: in default, a VU quotes (http.post), verifies (check), and waits (sleep). But the script doesn't say how many users perform it or for how long. That's the other half of a k6 script, and it lives outside default, in a special object: export const options. With two fields —vus and duration— you define your test's scale: vus: 10 puts ten virtual users, duration: '30s' keeps them running thirty seconds. It's the cast sheet that decides whether your test is a rehearsal with one actor or a show with hundreds.
Separating the script from the configuration is a deliberate and powerful design decision. The same default —the same user behavior— can run as a 1 VU / 10 s smoke test or as a 100 VUs / 5 min load test, just by changing options. You don't rewrite what the user does; you change how many and for how long. In this lesson we open options, understand vus and duration, and see the load scale with real numbers —from 5 VUs to 10, from 20 iterations to 100—.
Connection to the module: lessons 2 to 5 filled the script (the default function and its pieces); this one puts in the cast sheet. k6's options is shown as content; in the Python generator, vus and duration are arguments that are actually run, and you'll see the load grow as you raise them. Everything labeled as Python output was measured in this environment with Python 3.14.0 against the canonical API. Here the load is flat —a fixed number of VUs for a fixed duration—; making it vary over time (ramps, steps, spikes with stages) is module 4.
The play's cast sheet
Think of it this way. Pick up the play from lesson 1. The script says what a character does; but before opening night, the director signs a cast sheet that decides two things the script doesn't touch: how many actors take the stage and for how long the show lasts. The same play can be staged as an intimate reading with one actor and twenty minutes, or as a grand production with a hundred actors and three hours —with the same script, just changing the cast sheet—.
options is that cast sheet. vus is how many actors (virtual users) take the stage at once; duration is how long the show lasts. The script (default) doesn't change: it still says "quote, verify, wait." What options decides is the scale: a rehearsal with one VU or a premiere with a hundred. And because it's separate from the script, you can scale your test —from smoke to load to stress— touching only these two numbers, without touching the user's behavior.
export const optionsis your test's cast sheet:vussays how many virtual users run at once anddurationfor how long. It lives separate from the script (default), so the same test scales from 1 VU to 100 by changing only two numbers, without rewriting what the user does.
options in k6 (content)
options is declared as an exported constant, at the top of the script, outside the default function. k6 reads it before starting the run to know how to set up the execution:
// quote_load.js — the cast sheet separated from the script.
// SHOWN AS CONTENT: k6 is not installed in this environment.
import http from 'k6/http';
import { check, sleep } from 'k6';
// The cast sheet: 10 virtual users for 30 seconds.
export const options = {
vus: 10,
duration: '30s',
};
const BASE_URL = 'http://localhost:8000';
// The script: doesn't change no matter how many VUs run.
export default function () {
const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(`${BASE_URL}/quote`, payload, params);
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
The two fields:
vus: 10. The number of virtual users that run in parallel. Ten VUs means ten copies of the script running at once, each in its own loop (lesson 2). Raisingvusraises the concurrency —how much simultaneous pressure the API receives—.duration: '30s'. How long the test runs, as a string with a unit:'30s'(seconds),'5m'(minutes),'1h'(hours). During that time, the VUs repeat the script in a loop; when it's met, k6 stops the run (with a brief graceful stop to let the in-progress iterations finish) and prints the summary.
With vus: 10 and duration: '30s', k6 starts ten VUs, lets them quote-verify-wait in a loop for thirty seconds, and counts everything that happened. Notice again: the script doesn't know there are ten VUs. You write the behavior of one; options decides how many.
Why options goes in the script (and not only on the command line)
k6 also accepts these values from the command line —k6 run --vus 10 --duration 30s quote_load.js— and it's sometimes handy for a quick test. But declaring options inside the script has advantages that make it the preferred way:
- The test is reproducible and versionable. The script carries its own configuration with it; whoever runs it gets the same load, without depending on remembering the right flags. It goes into version control alongside the code.
- It's self-documenting. Opening the file tells you at a glance what load it imposes —10 VUs, 30 s— without searching a command history.
- It scales to complex configurations. When you move from a flat load to profiles with
stages, thresholds, and scenarios (modules 4, 5, 6), all of that lives inoptions. A single object describes the whole shape of the test. Starting by putting it in the script prepares you for that.
The practical rule: put options in the script as your test's source of truth; use the command-line flags only for one-off experiments or to override a value temporarily. What goes into the repository is the script with its options.
The load scales, run in Python
In the Python generator, vus and duration are arguments —just like the options fields—: vus sets how many workers (threads) the pool starts, duration_s how long they run. Raising them raises the load, and we can see it. Let's run two configurations with the same think time (sleep(1)), changing only the cast.
First, 5 VUs for 4 seconds:
What to expect. With sleep(1), each VU does ~1 iteration per second; 5 VUs × ~4 s ≈ 20 iterations. Real output (5 VUs, 4 s, think 1000 ms):
----------------------------------------------------------
Python load generator -> 5 VUs / 4s / think 1000ms
----------------------------------------------------------
vus............: 5
real duration..: 4.04s
iterations.....: 20 (4.9/s)
checks.........: 100.00% (40 of 40)
status is 200....: 20 ok / 0 fail
price is correct.: 20 ok / 0 fail
http_errors....: 0
req_duration...: avg=3.09ms min=0.75ms max=7.91ms
----------------------------------------------------------
Now we raise the cast: 10 VUs for 10 seconds, same script, same think time:
What to expect. 10 VUs × ~10 s ≈ 100 iterations, and an iterations/s close to 10 (double the VUs = double the pace). Real output (10 VUs, 10 s, think 1000 ms):
----------------------------------------------------------
Python load generator -> 10 VUs / 10s / think 1000ms
----------------------------------------------------------
vus............: 10
real duration..: 10.21s
iterations.....: 100 (9.8/s)
checks.........: 100.00% (200 of 200)
status is 200....: 100 ok / 0 fail
price is correct.: 100 ok / 0 fail
http_errors....: 0
req_duration...: avg=5.52ms min=0.58ms max=110.15ms
----------------------------------------------------------
Compare the two runs, because they show how options governs the scale:
vus: 5 → 10. It's exactly thevusfield ofoptions, made an argument. By doubling the VUs, you double the concurrency on the API.iterations/s: 4.9/s → 9.8/s. The total pace almost doubled, like the VUs. Withsleep(1), each VU contributes ~1 iteration/s, so 5 VUs give ~5/s and 10 VUs give ~10/s. The load scales withvuspredictably.iterations: 20 → 100. Not only the VUs changed; the duration did too (4 s → 10 s). The total iterations are, roughly,vus × duration(withsleep(1)): 5 × 4 = 20, 10 × 10 = 100. That formula —the bridge between the cast and the iterations— is exactly the arithmetic lesson 7 takes apart.req_durationmax: 7.91 ms → 110.15 ms. With more VUs hitting at once, the slowest request grew (from 8 ms to 110 ms): the first signal that concurrency starts to make the API work. That spike under load is exactly what module 3's metrics study in depth (why the average looks good —5.52 ms— but the worst case tells another story). Here just notice that raisingvusisn't free: the API feels it.
vus and duration vs. iterations: two ways to end
duration ends the test by time: "run 30 seconds, do whatever iterations you do." It's the most common way for a sustained load. But there's an alternative worth knowing:
// End by number of iterations, not by time.
export const options = {
vus: 10,
iterations: 200, // in total, across all VUs (not 200 per VU)
};
With iterations: 200, k6 splits 200 iterations across the 10 VUs (20 each) and ends when they're completed, no matter how long they take. It's useful when you want an exact number of operations (for example, "process 200 bookings") instead of a fixed time. An important detail: iterations counts the shared total, not per VU —10 VUs and iterations: 200 are 20 laps per VU, not 200—. In this module we use duration (end by time), which is typical for load tests; I mention iterations so you recognize the option. What you must not mix accidentally is duration and iterations at once expecting both to rule: each one defines a different end criterion.
Common mistakes
Putting duration without a unit. What happens: someone writes duration: 30 (a number) instead of duration: '30s' (a string with a unit). Why it happens: in other contexts durations are numbers. How to detect it: k6 rejects the value or interprets it differently than expected; the test doesn't last what you thought. How to fix it: duration is a string with a unit: '30s', '5m', '1h'. The bare number isn't enough.
Confusing vus with the number of requests or iterations. What happens: someone sets vus: 100 expecting "100 requests" and is puzzled to see thousands in the summary. Why it happens: vus are concurrent users, each in a loop that does many iterations. How to detect it: the summary's iterations are many more than the VUs. How to fix it: remember that vus is concurrency, not volume. The total volume is, approximately, vus × duration / (think + request time). If you want an exact volume, use iterations.
Believing that raising vus to a huge number "tests more" for free. What happens: someone jumps to vus: 5000 at once on their first test. Why it happens: they think more VUs is always better. How to detect it: the machine that generates the load (not the API) saturates —each VU consumes memory and CPU in the generator—, and the results get polluted. How to fix it: raise the VUs in a stepped, realistic way, and remember the generator also has a limit. Also, jumping straight to a high load is exactly what a ramped profile (module 4) avoids: raising gradually to see where it starts to hurt, not just that it hurts.
Exercises
Exercise 1 — Write the cast sheet. Write (in k6, as content) the options for a smoke test of 1 virtual user for 30 seconds, and then that of a load test of 50 virtual users for 5 minutes. What part of the script changes between the two, and what part doesn't?
See solution
Smoke test:
export const options = { vus: 1, duration: '30s' };
Load test:
export const options = { vus: 50, duration: '5m' };
What changes between the two is only options (the cast: how many VUs and how long). What doesn't change is the default function —the script, what each user does—. That separation is exactly the advantage of options: the same test scales from smoke to load without touching the user's behavior.
Exercise 2 — Predict the iterations. With a script that has sleep(1), how many approximate iterations do you expect from each options? (a) { vus: 5, duration: '4s' }; (b) { vus: 10, duration: '10s' }; (c) { vus: 20, duration: '10s' }.
See solution
With sleep(1), each VU does ~1 iteration per second, so total iterations ≈ vus × duration:
- (a) 5 × 4 = ~20 iterations (matches the lesson's real run: 20).
- (b) 10 × 10 = ~100 iterations (matches the real run: 100).
- (c) 20 × 10 = ~200 iterations. Double the VUs of (b), double the iterations.
The formula works because the sleep(1) fixes the pace at ~1 lap per VU per second. With another think time, the pace changes (lesson 5), but the idea —iterations grow with vus and with duration— holds.
Exercise 3 — duration or iterations. For each goal, say whether you'd use duration or iterations in options, and why: (a) "I want to subject the API to sustained load for 10 minutes"; (b) "I want to process exactly 500 bookings and see how long they take in total"; (c) "I want a quick 15-second smoke test."
See solution
- (a)
duration: '10m': you want load for a fixed time, no matter how many iterations come out. It's the typical case of a sustained load test. - (b)
iterations: 500: you want an exact number of operations (500 bookings), not a time. k6 splits the 500 across the VUs and ends when they're completed. - (c)
duration: '15s': a smoke test is defined by a short time of "does it even respond well?", so ending by time is the natural choice.
Summary and next step
In this lesson you put in the cast sheet. export const options lives outside the script and defines the test's scale with two fields: vus (how many virtual users run in parallel) and duration (for how long, as a string with a unit: '30s', '5m'). Separating the configuration from the behavior is what lets you scale the same test from 1 VU to 100 by changing only two numbers. You ran it for real: 5 VUs / 4 s gave 20 iterations, 10 VUs / 10 s gave 100 —the load scales with vus and duration predictably (iterations ≈ vus × duration with sleep(1))—, and you saw the first signal that raising vus isn't free: the slowest request went from 8 ms to 110 ms on doubling the concurrency. You also met iterations as an alternative way to end (by number of operations, not by time).
Before moving on you should be able to: write an options with well-formed vus and duration; explain why the configuration goes separate from the script and why it's better to put it in the script; and estimate the iterations of a flat load from vus, duration, and the think time.
That formula that keeps appearing —iterations ≈ vus × duration— and the confusion around it are the subject of lesson 7: the difference between VUs and iterations, and how to read the k6 summary (the checks, iterations, vus blocks) where all these numbers appear together.
Resources
- k6 options —
options— the reference for theoptionsobject and all its fields (vus,duration,iterations, and more). The exact source of this lesson. - How to set options — script vs. command line — the order of precedence and why declaring them in the script is the preferred way. The "why" of putting
optionsin the file. - Running a test —
k6 run— howvusanddurationgovern the run, with and without flags. How the test that's content here is launched. ThreadPoolExecutor(max_workers=...)— Python documentation — howvustranslates into the number of pool workers that run the load. The engine of the measured runs.