QA How-To
K6 scenarios and executors (2026)
Master k6 scenarios and executors in 2026: ramping-vus, arrival-rate, multi-scenario loads, CI profiles, metrics, and interview Q&A for performance QA.
18 min read | 2,288 words
TL;DR
k6 scenarios and executors define named workloads and how virtual users or arrival rates run them. Prefer ramping/constant VUs for session concurrency and constant/ramping arrival-rate for RPS-style goals, then combine scenarios with tags for realistic systems.
Key Takeaways
- Scenarios name workloads; executors schedule how iterations start.
- Use VU executors for concurrent sessions and arrival-rate for throughput SLOs.
- Size preAllocatedVUs and maxVUs from rate times iteration duration.
- Multi-scenario suites model mixed browse, checkout, and batch traffic.
- Tags, startTime, and gracefulStop make results interpretable and clean.
- Split smoke and load profiles with environment variables for CI.
- Treat dropped_iterations and missed arrival rates as performance signals.
k6 scenarios and executors are how modern Grafana k6 scripts model realistic traffic shapes instead of a single flat virtual-user ramp. In 2026, most performance interviews and production load jobs expect you to declare named scenarios, pick the right executor, and combine them when APIs, browsers, and batch jobs share one test run. If you still only use stages on the root options object, you can pass basic checks, but you will struggle with mixed workloads, arrival-rate capacity tests, and clean CI gates.
This guide walks through every built-in executor, shows runnable scenario configs, explains when to prefer VUs versus arrival rate, and covers tags, environments, graceful stop, and multi-scenario orchestration. Pair it with performance testing with k6 scripts for scripting patterns and designing a load model for traffic math.
TL;DR
| Executor | Models | Best for |
|---|---|---|
| shared-iterations | Fixed total work split across VUs | Smoke, fixed batch jobs |
| per-vu-iterations | Each VU runs N iterations | Even work per user |
| constant-vus | Fixed concurrent users | Steady open sessions |
| ramping-vus | VU count over time | Classic soak and peak ramps |
| constant-arrival-rate | Fixed iterations started per time | Throughput SLOs |
| ramping-arrival-rate | Arrival rate over time | Peak event launches |
| externally-controlled | Runtime scale via API | Interactive labs |
Pick VU executors when you care about concurrent sessions. Pick arrival-rate executors when you care about requests started per second regardless of response time.
1. What k6 Scenarios and Executors Actually Are
A scenario is a named workload definition inside options.scenarios. Each scenario points at a JavaScript function (the default export or a named export), chooses an executor, and sets timing, VUs, iterations, or arrival rate. Multiple scenarios can run in the same process and share code helpers while emitting separate metrics tagged by scenario name.
An executor is the scheduling algorithm k6 uses to start iterations. It answers: how many VUs, how many iterations, how fast new iterations begin, and how long the scenario lasts. Confusing VUs with throughput is the most common modeling error. If response times climb, a fixed VU count produces fewer completed requests per second. Arrival-rate executors keep starting work at the target pace until maxVUs is exhausted.
Scenarios replaced the older pattern of one global VU plan for most serious suites. You still can use top-level vus, duration, and stages for simple scripts. For anything with mixed read/write ratios, spike plus soak, or separate browser and API traffic, scenarios are the right abstraction.
2. Minimal Script with One Scenario
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
browse_catalog: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '1m', target: 0 },
],
gracefulRampDown: '30s',
tags: { surface: 'catalog' },
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
export default function () {
const res = http.get('https://test.k6.io/');
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
Run with:
k6 run script.js
You should see scenario-aware summary output. The function name defaults to the default export. To target a named export, set exec: 'checkoutFlow' on the scenario and export export function checkoutFlow() { ... }.
3. Executor Deep Dive: Iteration-Based Schedulers
shared-iterations
Total iterations are shared across VUs. Faster VUs grab more work. Good for "process this backlog of 10,000 jobs" smoke tests where fairness per VU does not matter.
export const options = {
scenarios: {
batch_import: {
executor: 'shared-iterations',
vus: 10,
iterations: 1000,
maxDuration: '10m',
},
},
};
per-vu-iterations
Each VU runs exactly iterations times. Total work equals vus * iterations. Use when every synthetic user must complete the same journey count (for example, five checkouts per account type).
export const options = {
scenarios: {
even_journeys: {
executor: 'per-vu-iterations',
vus: 20,
iterations: 5,
maxDuration: '15m',
},
},
};
| Executor | Total iterations | VU fairness | Typical use |
|---|---|---|---|
| shared-iterations | Fixed total | Unfair (fast VUs do more) | Fixed batch size |
| per-vu-iterations | vus * iterations | Fair per VU | Symmetric user loops |
4. Executor Deep Dive: VU-Based Schedulers
constant-vus
Hold a fixed number of concurrent VUs for a duration. Classic closed model. Easy to reason about session concurrency, weaker for pure RPS targets.
export const options = {
scenarios: {
steady_users: {
executor: 'constant-vus',
vus: 50,
duration: '10m',
},
},
};
ramping-vus
Change VU count over stages. This is the familiar JMeter-style ramp that most teams start with.
export const options = {
scenarios: {
peak_ramp: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '3m', target: 0 },
],
gracefulRampDown: '1m',
},
},
};
gracefulRampDown lets in-flight iterations finish when VUs are scheduled to leave. Without it, you can cut journeys mid-checkout and pollute error rates.
5. Executor Deep Dive: Arrival-Rate Schedulers
Arrival-rate executors are open models. k6 starts iterations at a configured rate. If the system slows down, k6 allocates more VUs (up to maxVUs) to keep the start rate.
constant-arrival-rate
export const options = {
scenarios: {
api_rps: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 200,
},
},
};
Here k6 aims to start 100 iterations per second for five minutes. preAllocatedVUs should be high enough for expected concurrency: roughly rate * average iteration duration in seconds, with headroom.
ramping-arrival-rate
export const options = {
scenarios: {
launch_day: {
executor: 'ramping-arrival-rate',
startRate: 0,
timeUnit: '1s',
preAllocatedVUs: 100,
maxVUs: 500,
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '1m', target: 300 },
{ duration: '3m', target: 300 },
{ duration: '2m', target: 0 },
],
},
},
};
Use this for flash sales, ticket drops, and marketing spikes where the business cares about arrivals, not seated user count.
Capacity planning note
If k6 logs that it could not reach the arrival rate, either raise maxVUs, shorten iteration work, or accept that the SUT (or the load generator) is the limit. That message is a finding, not only a config bug. See finding a performance bottleneck.
6. externally-controlled and Interactive Scaling
export const options = {
scenarios: {
lab: {
executor: 'externally-controlled',
vus: 10,
maxVUs: 100,
duration: '1h',
},
},
};
Scale at runtime with the k6 REST API while the test runs (local k6 run with the API enabled, or cloud/controller workflows your org supports). This executor is excellent for exploratory performance labs and poor for fully automated CI gates that need deterministic shapes.
7. Multi-Scenario Workloads (Realistic Systems)
Production traffic is rarely one pure shape. A storefront might need:
- Browse traffic as ramping arrival rate
- Checkout as constant VUs with think time
- Admin reports as low constant arrival rate
import http from 'k6/http';
import { sleep } from 'k6';
const BASE = __ENV.BASE_URL || 'https://test.k6.io';
export const options = {
scenarios: {
browse: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
preAllocatedVUs: 30,
maxVUs: 120,
stages: [
{ duration: '2m', target: 40 },
{ duration: '5m', target: 40 },
{ duration: '1m', target: 0 },
],
exec: 'browse',
tags: { flow: 'browse' },
},
checkout: {
executor: 'constant-vus',
vus: 15,
duration: '8m',
exec: 'checkout',
startTime: '30s',
tags: { flow: 'checkout' },
},
reports: {
executor: 'constant-arrival-rate',
rate: 2,
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 2,
maxVUs: 10,
exec: 'reports',
tags: { flow: 'reports' },
},
},
};
export function browse() {
http.get(`${BASE}/`);
sleep(0.5);
}
export function checkout() {
http.get(`${BASE}/contacts.php`);
sleep(1);
}
export function reports() {
http.get(`${BASE}/news.php`);
sleep(0.2);
}
startTime offsets a scenario so checkout begins after browse warms caches. Tags make Grafana and the end-of-test summary filterable by flow.
8. Choosing k6 Scenarios and Executors: VU vs Arrival Rate
| Question | Prefer |
|---|---|
| Do we sell "concurrent users" as the risk? | VU executors |
| Do SLOs say "N orders/minute started"? | Arrival-rate executors |
| Does think time dominate the journey? | VU executors with sleep |
| Are we validating autoscaling on RPS? | Arrival-rate executors |
| Is this a fixed data migration job? | shared-iterations |
| Do we need CI determinism? | Avoid externally-controlled |
Also consider generator capacity. Arrival-rate tests can demand many more VUs when the SUT slows, which can crash the load generator before the SUT. Cap maxVUs deliberately and treat maxed VUs as a signal.
For API-only microbenchmarks without think time, arrival rate maps cleanly to service capacity. For SaaS products with long websocket sessions, constant or ramping VUs often match product language better.
9. Options That Pair with Scenarios
Important scenario fields:
exec: named function to runenv: scenario-specific environment variablestags: static tags on metrics from that scenariostartTime: delay before scenario startsgracefulStop: time to let iterations finish after the scenario endsgracefulRampDown: for ramping-vus when reducing VUs
export const options = {
scenarios: {
mobile_api: {
executor: 'constant-arrival-rate',
rate: 20,
timeUnit: '1s',
duration: '3m',
preAllocatedVUs: 20,
maxVUs: 80,
exec: 'mobile',
env: { CHANNEL: 'mobile' },
tags: { channel: 'mobile' },
gracefulStop: '30s',
},
},
};
export function mobile() {
// read __ENV.CHANNEL inside the function if needed
}
Global options still matter: thresholds, summaryTrendStats, discardResponseBodies, userAgent, insecureSkipTLSVerify (only in controlled lab settings), and cloud project settings when using Grafana Cloud k6.
10. Stages Helpers vs Scenarios
Top-level stages still work:
export const options = {
stages: [
{ duration: '1m', target: 10 },
{ duration: '3m', target: 10 },
{ duration: '1m', target: 0 },
],
};
That is equivalent to a single ramping-vus scenario without a name. Prefer explicit scenarios once you have more than one workload, need tags, or need arrival rate. Teams standardizing on scenarios make code review easier: every load shape is a named object in git.
When migrating, convert root vus + duration to constant-vus, root stages to ramping-vus, and introduce arrival-rate only after product owners define throughput goals.
11. CI Patterns for Scenario Suites
Separate profiles with environment switches:
const profile = __ENV.PROFILE || 'smoke';
const profiles = {
smoke: {
scenarios: {
smoke: {
executor: 'shared-iterations',
vus: 2,
iterations: 20,
},
},
},
load: {
scenarios: {
load: {
executor: 'constant-arrival-rate',
rate: 50,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 50,
maxVUs: 150,
},
},
},
};
export const options = profiles[profile];
k6 run -e PROFILE=smoke script.js
k6 run -e PROFILE=load script.js
Smoke scenarios should finish in minutes and catch broken assertions. Full load scenarios run on schedule or pre-release. Thresholds should be stricter in load profiles. Wire results into your pipeline as in add CI to a test framework, even when the "framework" is k6 rather than a UI runner.
12. Observability: Reading Scenario Metrics
k6 tags metrics with scenario automatically. In the summary, watch:
http_reqsanditerationsper scenariovusandvus_maxagainst arrival-rate demanddropped_iterationswhen the executor cannot schedule workhttp_req_durationwith scenario or flow tags- Custom
TrendandRatemetrics you define per business step
Export JSON or use the experimental Prometheus remote write / OpenTelemetry setups your platform provides. Scenario tags let you prove checkout latency regressed while browse stayed flat, which is impossible with a single blended average.
For deeper API-focused technique, continue with API performance testing tutorial.
13. Scenario Design Patterns Teams Reuse
Smoke + soak in one file: two scenarios, smoke with shared-iterations at startTime: '0s', soak with constant-vus starting after smoke via startTime, or keep them as separate CLI profiles for cleaner gates.
Spike on a plateau: ramping-arrival-rate stages that hold baseline, jump 5-10x for a short window, then return. Measure recovery time, not only peak errors.
Read/write split: 90% browse scenario, 10% write scenario, separate thresholds on write error rate.
Canary vs full: canary scenario targets a subset host via env base URL; full scenario targets the production-like URL in a later pipeline stage.
Document the scenario catalog in the repo README: name, executor, purpose, owner, and which release train runs it. Without that, scenarios accumulate like unowned tests.
14. Comparing k6 Executors to JMeter Thread Groups
| k6 concept | Rough JMeter analog |
|---|---|
| Scenario | Thread Group (named) |
| ramping-vus | Thread Group ramp + schedulers |
| constant-vus | Thread Group with ramp 0 and fixed count |
| constant-arrival-rate | Precise Throughput Timer / concurrency thread group patterns |
| shared-iterations | Throughput controller style fixed work (approximate) |
| tags + exec | Controllers + naming conventions |
k6 scenarios are code-first and reviewable in pull requests. JMeter GUI plans still dominate some enterprises. Knowing both mappings helps you migrate plans without losing load intent. See also JMeter vs k6 for load testing.
15. Worked Example: From Product Story to Scenario Config
Suppose product risk is: "Black Friday browse stays under 500 ms p95 at 200 searches started per second, while checkout holds 30 concurrent buyers with under 1% errors."
Translate requirements:
- Browse ->
ramping-arrival-ratepeaking atrate: 200pertimeUnit: '1s'. - Checkout ->
constant-vuswithvus: 30and realisticsleepthink time. - Thresholds -> separate checks on tagged metrics where possible; at minimum global
http_req_failedand duration SLOs. - Data -> unique carts per VU using
__VUand__ITERto avoid artificial lock contention. - Environment -> staging sized like production for the bottleneck tier, not a tiny shared DB.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';
const searchTrend = new Trend('search_duration', true);
const BASE = __ENV.BASE_URL;
export const options = {
scenarios: {
search: {
executor: 'ramping-arrival-rate',
startRate: 20,
timeUnit: '1s',
preAllocatedVUs: 150,
maxVUs: 400,
stages: [
{ duration: '3m', target: 200 },
{ duration: '10m', target: 200 },
{ duration: '2m', target: 0 },
],
exec: 'search',
tags: { flow: 'search' },
},
buy: {
executor: 'constant-vus',
vus: 30,
duration: '15m',
exec: 'buy',
startTime: '1m',
tags: { flow: 'buy' },
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{flow:search}': ['p(95)<500'],
'http_req_duration{flow:buy}': ['p(95)<1200'],
},
};
export function search() {
const res = http.get(`${BASE}/?q=k6`);
searchTrend.add(res.timings.duration);
check(res, { 'search 200': (r) => r.status === 200 });
}
export function buy() {
const res = http.get(`${BASE}/contacts.php`);
check(res, { 'buy page 200': (r) => r.status === 200 });
sleep(2);
}
Walk stakeholders through the mapping before the first full run. Agreement on executors prevents the classic post-test argument: "We only simulated 30 users, why did search die?" when search was actually an open 200 RPS model.
16. Troubleshooting Scenarios in Practice
Symptoms and first checks:
- Summary shows low RPS with high VUs: iterations are slow or blocked on think time; confirm whether that matches a closed model goal.
- Arrival rate not met: inspect
dropped_iterations, generator CPU, network, andmaxVUs. - Huge error spike only at ramp-down: add
gracefulRampDown/gracefulStop. - One scenario starves another: generator saturated; split tests across machines or reduce combined peak.
- Thresholds flake in CI: smoke profile too aggressive, or environment cold starts; add warm-up stages.
- Metrics look blended: missing tags or both flows calling the same untagged URL helper.
k6 run --out json=results.json script.js
k6 run --verbose script.js
Use a short local dry run with tiny rates before booking a full performance window. Scenario mistakes are cheap to fix in a two-minute smoke and expensive to fix after a two-hour soak.
Interview Questions and Answers
Q: What is a k6 scenario?
A named workload under options.scenarios that binds an executor, timing parameters, and a function to run. Multiple scenarios can run together with independent schedules and tags.
Q: When do you choose arrival-rate over ramping-vus?
When the requirement is expressed as starts per second or per minute and must hold even if latency rises. Arrival-rate is an open model; ramping-vus is closed and couples throughput to response time.
Q: What does preAllocatedVUs mean?
It is the pool of VUs k6 initializes for arrival-rate executors. Size it from expected concurrency: rate times iteration duration, plus buffer. Too low causes dropped iterations.
Q: How do multi-scenario tests help?
They model mixed traffic (browse, checkout, admin) with different shapes and tags so metrics isolate which flow failed.
Q: What is gracefulStop?
Time after a scenario's scheduled end for iterations already running to finish cleanly before k6 interrupts them.
Q: Why might k6 fail to meet a constant-arrival-rate?
SUT slowness, insufficient maxVUs, overloaded generator CPU/network, or iterations that simply take too long for the requested rate.
Q: How do you run only a smoke profile in CI?
Parameterize options with __ENV.PROFILE and select a small shared-iterations or short constant-vus scenario for pull requests.
Common Mistakes
- Treating VUs as RPS without measuring iteration duration.
- Setting arrival rate without enough preAllocatedVUs and maxVUs.
- Mixing unrelated journeys in one default function without tags.
- Omitting gracefulRampDown and failing mid-transaction.
- Using externally-controlled in unattended CI.
- Copying production peak RPS into a tiny shared staging DB.
- Ignoring dropped_iterations in the summary.
- Forgetting startTime offsets when one flow depends on warm caches.
- Leaving discardResponseBodies off for large payloads and OOMing the generator.
- Keyword-stuffing thresholds without scenario-specific tags.
- Never documenting which scenario is the release gate.
- Running all heavy scenarios on every commit instead of smoke/load split.
Conclusion
k6 scenarios and executors turn a simple script into a controlled, reviewable load model. Master the seven executors, prefer arrival rate for throughput SLOs and VUs for concurrent session risk, and compose multi-scenario suites with tags and startTime offsets. Wire smoke and load profiles into CI so every merge gets a fast signal and releases get a real shape.
Next, deepen pass/fail criteria with thresholds and checks in the companion guide, and validate your numbers against a deliberate load model design before you scale generator size.
Interview Questions and Answers
Explain k6 scenarios and executors to a hiring manager.
Scenarios let us name and isolate workloads in one script. Executors choose the schedule: fixed iterations, fixed or ramping VUs, or open-model arrival rates. That lets QA express business load shapes in code reviewable pull requests.
When is constant-arrival-rate better than constant-vus?
When the SLO or capacity question is about how many iterations start per time unit. constant-vus holds concurrency, so throughput falls if latency rises. Arrival-rate keeps starting work until maxVUs is hit.
How do you size preAllocatedVUs?
Estimate concurrent iterations as target rate multiplied by average iteration duration in seconds, then add headroom. Set preAllocatedVUs near that estimate and maxVUs higher for slowdowns.
How would you model browse and checkout together in k6?
Two scenarios with different executors and exec functions, tagged by flow. Browse might use ramping-arrival-rate; checkout might use fewer constant VUs with think time. Separate thresholds by tag where needed.
What does gracefulRampDown solve?
When VU count decreases, gracefulRampDown gives active iterations time to finish instead of aborting mid-flow, which reduces false error spikes during planned ramp-down.
How do you keep k6 scenario tests usable in CI?
Provide a smoke profile with small shared-iterations or short duration, and a load profile for nightly or pre-release. Parameterize with environment variables and fail the build on thresholds.
What is an open model versus a closed model in load testing?
Closed models fix concurrent users; throughput depends on response time. Open models fix arrival rate; concurrency grows if the system slows. k6 VU executors are closed; arrival-rate executors are open.
Frequently Asked Questions
What are k6 scenarios and executors?
Scenarios are named workload entries under options.scenarios. Executors are the scheduling modes (such as ramping-vus or constant-arrival-rate) that decide how iterations and VUs start over time.
Which k6 executor should I use for a spike test?
ramping-arrival-rate or ramping-vus with a short, steep stage works well. Arrival-rate spikes map better when the business measures starts per second during the event.
What is the difference between shared-iterations and per-vu-iterations?
shared-iterations splits a fixed total work pool across VUs (faster VUs do more). per-vu-iterations gives each VU the same iteration count, so total work is vus times iterations.
Why does k6 report dropped iterations?
Usually the arrival-rate target needs more VUs than maxVUs allows, or the generator cannot schedule work fast enough. Raise capacity, reduce iteration cost, or lower the rate and record the limit.
Can I run multiple k6 scenarios at once?
Yes. Define multiple entries under options.scenarios with their own executors, exec functions, tags, and optional startTime offsets so mixed traffic runs in one test.
Do I still need top-level stages if I use scenarios?
No. Prefer scenario-local stages for ramping executors. Top-level stages remain fine for tiny single-workload scripts, but scenarios scale better for real suites.
How do I pass different base URLs per scenario?
Use the scenario env field for scenario-specific variables, or branch on tags and __ENV values inside each exec function while keeping secrets out of source control.
Related Guides
- K6 load testing tutorial: Step by Step (2026)
- K6 thresholds and checks (2026)
- Performance testing with k6 scripts: Step by Step (2026)
- AI test data generation with Faker and LLMs (2026)
- API error handling and negative testing: A Practical Guide (2026)
- Appium gestures and swipe: A Practical Guide (2026)