Resource library

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:

  1. Browse traffic as ramping arrival rate
  2. Checkout as constant VUs with think time
  3. 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 run
  • env: scenario-specific environment variables
  • tags: static tags on metrics from that scenario
  • startTime: delay before scenario starts
  • gracefulStop: time to let iterations finish after the scenario ends
  • gracefulRampDown: 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_reqs and iterations per scenario
  • vus and vus_max against arrival-rate demand
  • dropped_iterations when the executor cannot schedule work
  • http_req_duration with scenario or flow tags
  • Custom Trend and Rate metrics 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:

  1. Browse -> ramping-arrival-rate peaking at rate: 200 per timeUnit: '1s'.
  2. Checkout -> constant-vus with vus: 30 and realistic sleep think time.
  3. Thresholds -> separate checks on tagged metrics where possible; at minimum global http_req_failed and duration SLOs.
  4. Data -> unique carts per VU using __VU and __ITER to avoid artificial lock contention.
  5. 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, and maxVUs.
  • 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