{"title":"Model Realistic Workloads with k6 Scenarios","excerpt":"A script that walks every virtual user through browse, search and checkout sends more than three times the real write share at the database. This builds a four-journey k6 v2.3.0 workload, one scenario per journey with its own rate, pool and start time, and checks what the run delivered: printed per-scenario rates that divide by the whole test, a mix that only holds over a shared window, and a slow checkout that starved browse in a weighted single scenario but not in separate ones. It also shows the documented round-robin account pattern colliding on 89% of requests while making latency look faster.","canonicalUrl":"https://automationtester.in/blog/automation-tutorials/k6-scenarios-realistic-workload-models","category":{"name":"Automation Tutorials","slug":"automation-tutorials"},"tags":["k6","load-testing","performance-testing","workload-modeling","test-data","k6-scenarios"],"author":{"name":"Shashank Rawlani","url":"https://shashank.rawlani.com","linkedin":"https://www.linkedin.com/in/shashankrawlani/"},"datePublished":"2026-10-23T13:30:00.000Z","dateModified":"2026-10-23T13:30:00.000Z","coverImage":{"url":"https://ik.imagekit.io/automationtester/automationtester-in/blog/covers/v3/k6-scenarios-realistic-workload-models.webp","alt":"Four horizontal rows on a dark grid. On the left, three rows of green dots at different spacings, dense, medium and sparse, and below them a green circular loop with an arrowhead. Each row runs into its own rounded panel of a different height holding green and dim pills. The top two panels send a single green line on to the right edge. The tallest panel fans out into five green curves, each ending at its own small ringed circle. Beneath them, two dashed orange curves leave the same panel and converge on one orange crossed-out circle."},"contentHtml":"<div class=\"callout callout-info\"><strong>Quick answer:</strong> Give each user journey its own k6 scenario, with its own executor, rate, think time, VU pool and start time, instead of one function that walks every user through every journey. Drive browse, search and checkout with arrival-rate executors set from first-party counts, and pace a background poller the way its client paces itself. Bind each account that receives writes to one VU with <code>vu.idInTest</code>, not to an iteration number. Then prove the model ran: per-scenario counts over a shared window, drops per scenario, and zero write conflicts.</div>\n\n<p>A load test report blames the order database. Under 60 virtual users, checkout latency climbs, lock waits appear in the database metrics, and the team starts planning an index. Look at the script and every iteration does the same thing: list products, search, add to cart, pay. Every simulated user buys something. In production, one journey in ten is a checkout. The test sent more than three times the real write share at the database, from twenty seeded accounts reused in rotation, so some lock waits were the test queueing behind itself. The report was accurate about a run that resembled nothing in production.</p>\n\n<p>Part 1 of this series, on <a href=\"/blog/automation-tutorials/choose-smoke-load-stress-spike-soak-test\">choosing the test type from the release question</a>, kept to one endpoint. This part builds the workload a real release question needs: four journeys arriving at different rates, starting and stopping independently, with test data that cannot collide.</p>\n\n<div class=\"callout callout-warning\"><strong>Checked 2026-10-14:</strong> the newest k6 release is v2.3.0 (21 September 2026); the v1 line received v1.8.2 on 12 October 2026. All output below came from k6 v2.3.0 on macOS arm64, driving a small Node.js 26.0.0 stub shop on 127.0.0.1. The <code>--scenario</code> flag used once below first shipped in v2.3.0.</div>\n\n<h2 id=\"k6-mix-production-evidence\">What to pull from production before writing a scenario</h2>\n\n<p>A scenario has a handful of numbers in it, and each should trace to a measurement. The worked example uses a shop whose busiest hour, measured over the last 30 days, averaged 1,200 journey starts per minute: browse 60%, search 25%, checkout 10%, and order-status polling 5%. That split is the input. The table says where each number comes from and which option it sets.</p>\n\n<table>\n<thead><tr><th>Journey</th><th>First-party evidence</th><th>What it sets</th></tr></thead>\n<tbody>\n<tr><td>Browse</td><td>Sessions whose first request is a catalogue page, counted per minute; pages per session; gaps between page views</td><td><code>rate</code>, the number of requests per iteration, think time</td></tr>\n<tr><td>Search</td><td>Search requests per minute, and the share that go on to page 2</td><td><code>rate</code>, whether the iteration requests a second page</td></tr>\n<tr><td>Checkout</td><td>Orders per minute from the order table, and the time from add-to-cart to payment</td><td><code>rate</code>, the think time between the two writes, how many accounts the test needs</td></tr>\n<tr><td>Order polling</td><td>Clients with an order-status view open at once, and the polling interval in the client code</td><td><code>vus</code> and <code>sleep()</code>, or a rate if the client polls on a fixed timer</td></tr>\n</tbody>\n</table>\n\n<p>Write down whether the numbers are the busiest hour's average or its busiest minute; a reviewer cannot tell from the script. Also record whether checkouts rise with browsing or independently of it. A campaign email moves checkout on its own, which is a reason to give checkout a rate you can raise without touching the other journeys.</p>\n\n<p>An AI assistant can usefully cluster exported traces into candidate journeys. It is not a source for rates. Counts and correlations come from your own analytics or order data, aggregated before export so no user identifiers leave the system; if an assistant proposes a rate, find the query behind it first. Turning sessions into one flow's arrival rate is covered in the <a href=\"/blog/tools-reviews/generate-k6-load-model-traffic-assumptions\">guide to building a k6 load model from traffic assumptions</a>; this article combines flows.</p>\n\n<h2 id=\"k6-mix-every-user-buys\">The script where every user checks out</h2>\n\n<p>This is the shape that produced the misleading report. It calls three journey functions in sequence, so each iteration is one browse, one search and one checkout:</p>\n\n<pre><code class=\"language-javascript\">// everyone.js: every VU runs every journey, in order\nimport { sleep } from 'k6';\nimport { browse, search, checkout } from './mix.js';\n\nexport const options = { vus: 60, duration: '1m' };\n\nexport default function () {\n  browse();\n  sleep(1);\n  search();\n  sleep(1);\n  checkout();\n}\n</code></pre>\n\n<p>Against the stub, the server counted 1,083 product requests, 722 search requests, 361 add-to-cart writes and 361 payments. The journey ratio was 1:1:1, so checkout was a third of journeys instead of a tenth, and writes were 722 of 2,527 requests, 28.6% against the 7.8% the real mix produces. Nothing in the summary flags this, because the script did exactly what it said.</p>\n\n<p>The shape has two further costs. Every journey's rate is set by the duration of the whole chain, so a slow payment also slows browsing. And with one schedule, nothing can start earlier or stop sooner than anything else.</p>\n\n<h2 id=\"k6-mix-scenario-per-journey\">One scenario per journey</h2>\n\n<p>The k6 scenarios documentation describes scenarios as independent of each other and run in parallel, each able to execute a different exported function through <code>exec</code>, with its own environment variables and tags. That is the unit a journey needs. Here is the shop as four scenarios:</p>\n\n<pre><code class=\"language-javascript\">// mix.js\nimport http from 'k6/http';\nimport { check, sleep } from 'k6';\nimport { vu } from 'k6/execution';\n\nconst BASE = __ENV.BASE_URL || 'http://127.0.0.1:3471';\n\n// Journey starts per minute in the busiest hour: 1,200 in total.\n// browse 60%, search 25%, checkout 10%, order polling 5%.\nconst PER_MIN = { browse: 720, search: 300, checkout: 120 };\n\n// One account per VU id. Size it to at least the header's \"max VUs\".\nconst ACCOUNTS = Array.from({ length: 100 }, (_, i) =&gt; `acct-${String(i + 1).padStart(4, '0')}`);\nfunction ownAccount() {\n  const account = ACCOUNTS[vu.idInTest - 1];\n  if (!account) throw new Error(`no account for VU ${vu.idInTest}; grow the pool`);\n  return account;\n}\n\nconst think = (min, max) =&gt; sleep(min + Math.random() * (max - min));\n\nexport const options = {\n  scenarios: {\n    browse: {\n      executor: 'constant-arrival-rate',\n      exec: 'browse',\n      rate: PER_MIN.browse,\n      timeUnit: '1m',\n      startTime: '15s',\n      duration: '2m',\n      preAllocatedVUs: 50,\n    },\n    search: {\n      executor: 'constant-arrival-rate',\n      exec: 'search',\n      rate: PER_MIN.search,\n      timeUnit: '1m',\n      startTime: '15s',\n      duration: '2m',\n      preAllocatedVUs: 20,\n    },\n    checkout: {\n      executor: 'constant-arrival-rate',\n      exec: 'checkout',\n      rate: PER_MIN.checkout,\n      timeUnit: '1m',\n      startTime: '15s',\n      duration: '2m',\n      preAllocatedVUs: 15,\n      gracefulStop: '10s',\n    },\n    order_polling: {\n      executor: 'constant-vus',\n      exec: 'pollOrders',\n      vus: 4,\n      duration: '2m30s',\n      gracefulStop: '0s',\n    },\n  },\n};\n\nexport function browse() {\n  http.get(`${BASE}/products?page=1`, { tags: { name: 'list' } });\n  think(1, 2);\n  http.get(`${BASE}/products?id=${Math.floor(Math.random() * 5000)}`, { tags: { name: 'product' } });\n  think(1, 2);\n  http.get(`${BASE}/products?id=${Math.floor(Math.random() * 5000)}`, { tags: { name: 'product' } });\n}\n\nexport function search() {\n  http.get(`${BASE}/search?q=boots`);\n  think(1.5, 2.5);\n  http.get(`${BASE}/search?q=boots&amp;page=2`);\n}\n\nexport function checkout() {\n  const headers = { 'X-Account': ownAccount(), 'Content-Type': 'application/json' };\n  const add = http.post(`${BASE}/cart/items`, JSON.stringify({ sku: 'SKU-1', qty: 1 }), { headers });\n  think(2, 4);\n  const pay = http.post(`${BASE}/checkout`, '{}', { headers });\n  check(pay, { 'order placed': (r) =&gt; r.status === 201 });\n}\n\nexport function pollOrders() {\n  http.get(`${BASE}/orders`, { headers: { 'X-Account': ownAccount() } });\n  sleep(4);\n}\n</code></pre>\n\n<p>The rates are per minute because <code>rate</code> is an integer. At this shop's 20 journeys per second, a journey with a 2% share would be 0.4 per second, which cannot be written; per minute it is 24. A <code>timeUnit</code> of one minute keeps small shares exact. The think time sits between steps inside each journey, never at the end; the constant-arrival-rate page says the executor already paces iteration starts, so a trailing <code>sleep()</code> is unnecessary.</p>\n\n<p>Each scenario starts and stops on its own clock. The poller runs for the whole two and a half minutes, so user journeys, which start at 15 seconds through <code>startTime</code>, arrive into a system that already has background traffic. Checkout gets a shorter <code>gracefulStop</code> than the 30-second default, and the poller gets none; an unfinished poll is not worth waiting for.</p>\n\n<h3 id=\"k6-mix-poller-closed-loop\">The poller is the one closed loop</h3>\n\n<p>Arrival-rate executors suit user journeys, because users do not wait for each other. A poller may. If the client schedules its next poll only after the previous response arrives, a slower endpoint really does produce fewer polls, and a closed model reproduces that. Four VUs with a four-second sleep give 4 × 60 / 4 = 60 polls per minute, the 5% share, falling slightly as the endpoint slows, as the real clients would. A client that polls on a fixed timer is an open arrival process and belongs on <code>constant-arrival-rate</code>. Read the client code to decide.</p>\n\n<h3 id=\"k6-mix-pool-per-journey\">Sizing each pool from its own iteration time</h3>\n\n<p>Each arrival-rate scenario gets its own <code>preAllocatedVUs</code>, estimated in the docs as median iteration duration times rate, plus a margin. The run measured medians of 3.04 s for browse, 2.05 s for search and 3.82 s for checkout: 12 × 3.04 ≈ 36.5 busy VUs, 5 × 2.05 ≈ 10.3 and 2 × 3.82 ≈ 7.6, plus 4 pollers, about 58 in total. The <code>vus</code> gauge peaked at 62. Think time is the largest term in each product, which is why realistic pauses need far more VUs than a script that hammers one endpoint.</p>\n\n<h2 id=\"k6-mix-reading-achieved-mix\">Checking the mix the run delivered</h2>\n\n<p>Run it with the per-scenario summary, which <code>--summary-mode full</code> adds:</p>\n\n<pre><code class=\"language-bash\">k6 run --summary-mode full mix.js\n</code></pre>\n\n<p>The header shows what k6 resolved for each scenario before any traffic is sent: executor, rate, pool size, start time and graceful stop. Read it first:</p>\n\n<pre><code class=\"language-text\">scenarios: (100.00%) 4 scenarios, 89 max VUs, 2m45s max duration (incl. graceful stop):\n         * order_polling: 4 looping VUs for 2m30s (exec: pollOrders)\n         * browse: 12.00 iterations/s for 2m0s (maxVUs: 50, exec: browse, startTime: 15s, gracefulStop: 30s)\n         * checkout: 2.00 iterations/s for 2m0s (maxVUs: 15, exec: checkout, startTime: 15s, gracefulStop: 10s)\n         * search: 5.00 iterations/s for 2m0s (maxVUs: 20, exec: search, startTime: 15s, gracefulStop: 30s)\n</code></pre>\n\n<p>From the four scenario blocks, trimmed to the execution lines:</p>\n\n<pre><code class=\"language-text\">█ SCENARIO: browse\n  http_reqs..................: 4320   28.810905/s\n  iterations.................: 1440   9.603635/s\n█ SCENARIO: checkout\n  http_reqs..................: 480   3.201212/s\n  iterations.................: 240   1.600606/s\n█ SCENARIO: order_polling\n  http_reqs..................: 152   1.013717/s\n  iterations.................: 148   0.98704/s\n█ SCENARIO: search\n  http_reqs..................: 1202   8.016368/s\n  iterations.................: 601    4.008184/s\n</code></pre>\n\n<p>Browse was configured at 12 iterations per second, and the summary says 9.6 with nothing dropped. The printed rate is the count divided by the whole test's duration: 1,440 over 149.9 seconds is 9.6, while 1,440 over browse's own two minutes is exactly 12. Grafana's scenario example shows the same: a scenario that ran 100 iterations in 2.8 seconds is reported at 8.7 per second. When scenarios have different windows, divide each count by its own duration.</p>\n\n<p>The same window problem distorts the mix. Over the whole run, polling was 148 of 2,429 iterations, 6.1%, because it ran 30 seconds longer than everything else. In the two minutes all four journeys shared, the pollers would have completed about 120 polls (4 × 120 s ÷ 4.006 s per loop), giving 1,440, 601, 240 and 120: 60%, 25%, 10% and 5%, as planned.</p>\n\n<p>Polling shows 152 requests but 148 iterations. With <code>gracefulStop: '0s'</code>, the four pollers were cut off during their final sleep; the closing progress line reports 2,429 complete and 4 interrupted iterations, and interrupted iterations are not counted in <code>iterations</code>. If you reconcile counts against a plan, add the interrupted ones back.</p>\n\n<p>Journey mix and request mix are different numbers. Browse makes three requests per journey, so its 60% of journeys became 4,320 of 6,154 requests, 70.2%, and checkout's 10% became 7.8%. A capacity plan for the database needs the write share of requests; a statement about users needs the journey share. Write down which one each figure is.</p>\n\n<h2 id=\"k6-mix-weighted-branch\">Why not one scenario with weighted branches</h2>\n\n<p>A common middle ground keeps one arrival-rate scenario and picks a journey at random inside each iteration:</p>\n\n<pre><code class=\"language-javascript\">// weighted.js: correct average mix, one shared schedule and VU pool\nimport { browse, search, checkout } from './mix.js';\n\nconst PER_MIN = { browse: 720, search: 300, checkout: 120 };\nconst TOTAL = PER_MIN.browse + PER_MIN.search + PER_MIN.checkout; // 1,140\n\nexport const options = {\n  scenarios: {\n    shop: {\n      executor: 'constant-arrival-rate',\n      rate: TOTAL,\n      timeUnit: '1m',\n      duration: '2m',\n      preAllocatedVUs: 85, // the same 50 + 20 + 15 the separate scenarios get\n    },\n  },\n};\n\nexport default function () {\n  const r = Math.random() * TOTAL;\n  if (r &lt; PER_MIN.browse) browse();\n  else if (r &lt; PER_MIN.browse + PER_MIN.search) search();\n  else checkout();\n}\n</code></pre>\n\n<p>With a healthy system, this gives the right average mix. The difference shows when one journey degrades. With the stub's payment step slowed to about 20 seconds on average, both designs ran for two minutes with the same 85 VUs for user journeys; the separate-scenario run used <code>--scenario browse,search,checkout</code> to leave the poller out.</p>\n\n<p>The weighted version dropped 172 iterations. The server counted 1,339 browse journeys instead of 1,440, 552 searches instead of 600 and 218 checkouts instead of 240: slow checkouts held VUs that the healthy journeys needed, and they lost 7 to 8% of their traffic. The drops carry only the scenario tag <code>shop</code>. The branch is chosen inside the iteration, so an iteration that never started never chose a journey, and nothing in the results says which one it would have been.</p>\n\n<p>The separate-scenario version also dropped iterations: 154. All of them were tagged <code>scenario=checkout</code>. Browse ran 1,441 iterations and search 601, their full schedules. The failure stayed in the journey that caused it, and the browse and search latencies were measured at their planned rates.</p>\n\n<p>The cost is sizing four pools instead of one, and a shared pool absorbs variance between journeys that separate pools do not. A pool too small for its own journey drops iterations from configuration, not from the system; drops in the first seconds point there.</p>\n\n<h2 id=\"k6-mix-account-ownership\">Giving every write an owner</h2>\n\n<p>Read-only journeys can share data; nobody locks a product page. Two concurrent checkouts on one account contend for its cart and order rows, and the system answers with a 409, a lock wait or a retry at a rate production never sees, because one person rarely checks out from two places at once.</p>\n\n<p>The k6 data parameterization page offers a round-robin pattern for this and says it \"avoids collisions during a run\": index the dataset with <code>scenario.iterationInTest % vus</code>. That holds only while every iteration takes the same time. An iteration number is assigned when an iteration starts, so if iteration 3 is slow, iteration 23 can start on another VU while 3 is still running, and with 20 accounts both use the same one. To test it, the stub rejects a write with 409 when the account already has a write in flight, and payments take anywhere from 50 to 1,550 ms:</p>\n\n<pre><code class=\"language-javascript\">// ownership.js: the same checkout, two ways of choosing the account\nimport http from 'k6/http';\nimport { Counter } from 'k6/metrics';\nimport { scenario, vu } from 'k6/execution';\n\nconst BASE = 'http://127.0.0.1:3471';\nconst VUS = 20;\nconst accounts = Array.from({ length: VUS }, (_, i) =&gt; `acct-${String(i + 1).padStart(4, '0')}`);\nconst writeConflicts = new Counter('write_conflicts');\n\nexport const options = {\n  scenarios: {\n    checkout: { executor: 'constant-vus', vus: VUS, duration: '30s' },\n  },\n  thresholds: {\n    write_conflicts: ['count==0'],\n  },\n};\n\nexport default function () {\n  const account = __ENV.MODE === 'vu'\n    ? accounts[vu.idInTest - 1]                  // right: one account per VU\n    : accounts[scenario.iterationInTest % VUS];  // wrong: round-robin by iteration\n  const headers = { 'X-Account': account };\n  for (const res of [\n    http.post(`${BASE}/cart/items`, '{\"sku\":\"SKU-1\"}', { headers }),\n    http.post(`${BASE}/checkout`, '{}', { headers }),\n  ]) {\n    if (res.status === 409) writeConflicts.add(1);\n  }\n}\n</code></pre>\n\n<p>The round-robin run failed its threshold and exited with code 99:</p>\n\n<pre><code class=\"language-text\">write_conflicts\n✗ 'count==0' count=12519\n\nhttp_req_duration..............: avg=43.37ms  min=32µs    med=92µs     max=1.54s p(90)=20.14ms  p(95)=147.92ms\n  { expected_response:true }...: avg=404.26ms min=19.21ms med=54.28ms  max=1.54s p(90)=1.23s    p(95)=1.39s\nhttp_req_failed................: 89.29% 12519 out of 14020\niterations.....................: 7010   225.045077/s\n</code></pre>\n\n<p>Read the latency line before the failure rate. The median request took 92 microseconds, because a rejected write returns at once and the closed-loop VU immediately starts another iteration: 7,010 iterations in 30 seconds. The <code>MODE=vu</code> run, with each VU keeping its own account, completed 736 iterations with zero conflicts and exited 0. Shared write data added errors, and it also made the system look faster and nearly ten times busier. The collisions were not caused by the random delay alone: with the payment delay fixed at 50 ms, the round-robin pattern still put 25.5% of requests into conflicts, because each early 409 shortens an iteration and shifts every later assignment.</p>\n\n<p>Binding accounts to <code>vu.idInTest</code> works because a VU runs one iteration at a time, as the arrival-rate allocation page states. The <code>k6/execution</code> reference adds two details: VU ids start at 1 and are unique across the whole test, including distributed runs, while <code>scenario.iterationInTest</code> is unique only within a scenario, so two scenarios indexing one dataset by iteration collide with each other. Size the account pool to at least the header's max VUs, 89 here, and fail loudly when a VU id has no account, as <code>ownAccount()</code> does. Load real seeded accounts with <code>SharedArray</code>; the <a href=\"/tools/test-data-generator\">Test Data Generator</a> can produce a bounded set to seed.</p>\n\n<p>The data-ownership evidence is three numbers: <code>write_conflicts</code> at zero, the header's max VUs against the account pool size, and the distinct accounts the server saw writes from. In the full run, 15 accounts received writes, one per checkout VU.</p>\n\n<h2 id=\"k6-mix-symptoms-causes\">Four symptoms and the cause behind each</h2>\n\n<p><strong>A journey's share is a few points off plan.</strong> Usually one scenario started earlier or ran longer. Recompute over the shared window before changing any rate.</p>\n\n<p><strong>Drops in one scenario only.</strong> In the first seconds, that pool is too small; after the rate held, that journey's latency grew. The other journeys' numbers stay valid.</p>\n\n<p><strong>Drops in every scenario at the same moment.</strong> Suspect the load generator before the system; <a href=\"/blog/automation-tutorials/debug-k6-results-load-generator-bottlenecks\">debugging k6 results when the load generator is the bottleneck</a> covers that check.</p>\n\n<p><strong>A write journey's 4xx rate rises while its latency improves.</strong> Shared test data. Real contention makes latency worse, not better; compare max VUs with distinct accounts written.</p>\n\n<p>Turning these readings into pass and fail conditions per scenario is the subject of Part 2, on <a href=\"/blog/automation-tutorials/k6-thresholds-fail-ci-right-reasons\">thresholds that fail CI for the right reasons</a>. The <a href=\"/tools/k6-generator\">k6 Script Generator</a> produces a single-request starting point that you can split into journey functions.</p>\n\n<h2 id=\"k6-mix-apply-today\">Apply this now</h2>\n\n<p>Count how many journeys one iteration of your current load script performs. If it is more than one, split them into exported functions, each with its own scenario and <code>exec</code>. Pull last month's busiest hour from your analytics and order table, and write the counts and their source above <code>PER_MIN</code>. Check how your order-status client paces its polls before choosing its executor. Replace write data indexed by iteration with accounts indexed by <code>vu.idInTest</code>, and add a conflict counter. Run once with <code>--summary-mode full</code>, and keep the header, the per-scenario counts over the shared window, the drops per scenario and the conflict count together as the evidence that the test ran the workload you wrote down.</p>\n\n<h2 id=\"k6-mix-faq\">Questions about modelling mixed traffic</h2>\n\n<h3 id=\"k6-mix-faq-how-many-scenarios\">How many scenarios is too many?</h3>\n<p>Each scenario needs its own evidence and its own pool, so a journey that carries under a percent of traffic and touches no distinct backend path is usually better folded into a neighbour. Split when a journey has its own rate source, its own write path, or its own failure you need attributed.</p>\n\n<h3 id=\"k6-mix-faq-select-one\">Can I run one journey on its own while developing?</h3>\n<p>Yes, from k6 v2.3.0: <code>k6 run --scenario checkout mix.js</code> runs only the checkout scenario with its configured load, and a comma-separated list selects several. Thresholds tagged for the scenarios you excluded are skipped, with a warning.</p>\n\n<h3 id=\"k6-mix-faq-same-user-twice\">What if real users do browse and then check out?</h3>\n<p>Then model that sequence as its own journey with its own measured rate, for example browse-then-buy sessions per minute. What fails is assuming every session ends in a purchase. Measured sequences belong in a scenario; assumed ones do not.</p>\n\n<h2 id=\"k6-mix-references\">Primary references</h2>\n<ul>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/\">Scenarios overview in the k6 manual</a>: independent, parallel scenarios; the <code>startTime</code>, <code>gracefulStop</code>, <code>exec</code>, <code>env</code> and <code>tags</code> options; the per-scenario output whose printed rates divide by the whole test; and running selected scenarios with <code>--scenario</code>.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/advanced-examples/\">Advanced scenario examples from Grafana</a>: sequencing workloads, per-scenario tags and environment variables, and the automatic <code>scenario</code> tag on every metric.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/\">Executor list for k6 scenarios</a>: the six executors and the statement that no VU can be mapped reliably to a specific iteration.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-arrival-rate/\">constant-arrival-rate executor page</a>: required options, integer <code>rate</code>, fractional spacing of starts, and no <code>sleep()</code> at the end of an iteration.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-vus/\">constant-vus executor page</a>: a fixed VU count looping for a duration, and throughput set by iteration time.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/\">Open versus closed models in k6</a>: why a closed loop's rate follows response time, which is the behaviour a response-paced poller has.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/arrival-rate-vu-allocation/\">How k6 allocates VUs for arrival rates</a>: one iteration per VU at a time, the <code>preAllocatedVUs</code> estimate, and drops when no VU is free.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/dropped-iterations/\">The dropped_iterations concept page</a>: configuration-related versus system-related drops, and their timing.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/graceful-stop/\">Graceful stop behaviour</a>: the 30-second default and interrupted iterations at the end of a scenario.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/javascript-api/k6-execution/\">The k6/execution module reference</a>: <code>vu.idInTest</code> unique across the test and one-based, and <code>scenario.iterationInTest</code> unique within a scenario.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/examples/data-parameterization/\">Data parameterization examples</a>: <code>SharedArray</code>, per-VU data with <code>vu.idInTest</code>, the per-scenario caveat on <code>iterationInTest</code>, and the round-robin pattern tested above.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/javascript-api/k6-data/sharedarray/\">SharedArray API</a>: one read-only copy of test data shared by all VUs.</li>\n<li><a href=\"https://grafana.com/docs/k6/latest/results-output/end-of-test/\">End-of-test summary modes</a>: <code>--summary-mode full</code> and its per-scenario blocks.</li>\n<li><a href=\"https://github.com/grafana/k6/releases/tag/v2.3.0\">Release notes for k6 v2.3.0</a>: the <code>--scenario</code> flag and how selection treats excluded scenarios' thresholds.</li>\n</ul>\n"}