k6 Scenarios and Executors: Modeling Realistic Load Shapes

How k6's scenarios and executors let you model open-system arrival-rate traffic and closed-system concurrent-user traffic precisely.

· By perf-test.com Editorial · AI-assisted
k6scenariosload-modeling

k6’s scenarios and executors are its answer to the closed-system-vs-open-system modeling problem covered elsewhere on this site (see the JMeter Thread Groups article) — and k6 makes the distinction explicit and first-class, rather than something you have to work around with plugins.

Executors: the load-shape building blocks

  • constant-vus — a fixed number of virtual users running for a set duration (closed system, similar to a plain JMeter Thread Group).
  • ramping-vus — virtual user count changes over defined stages (ramp up, hold, ramp down) — similar to JMeter’s Ultimate Thread Group plugin, but built in.
  • constant-arrival-rate — a fixed number of iterations started per time unit, independent of how long each iteration takes (true open-system modeling) — k6 automatically adds virtual users as needed to sustain the target rate even if individual iterations run long.
  • ramping-arrival-rate — arrival rate changes over defined stages, the open-system equivalent of ramping-vus.
  • per-vu-iterations / shared-iterations — fixed total iteration counts, useful for “run this exact number of operations” tests rather than time-based ones.

Why constant-arrival-rate matters

This executor directly solves the problem covered in this site’s pacing and Little’s Law articles: if you want to test “what happens at 500 requests/second,” constant-arrival-rate lets you specify that target directly, and k6 manages virtual user count dynamically to hit it — rather than you manually calculating and tuning thread count and think time to approximate a target rate indirectly, as you would with a plain concurrent-user model.

Defining multiple scenarios in one script

export const options = {
  scenarios: {
    browsing: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },
        { duration: '5m', target: 50 },
        { duration: '2m', target: 0 },
      ],
    },
    checkout_api: {
      executor: 'constant-arrival-rate',
      rate: 100,
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 50,
    },
  },
};

This runs two distinct load shapes concurrently within one test, modeling different traffic types (browsing users vs. a steady API call rate) the way a realistic production traffic mix actually looks, rather than one uniform load pattern.

preAllocatedVUs and maxVUs

For arrival-rate executors, preAllocatedVUs reserves virtual user capacity upfront (avoiding the overhead of spinning up new VUs mid-test), and maxVUs caps how high k6 can scale VU count if response times slow down and more concurrent VUs are needed to sustain the target arrival rate — if maxVUs is hit, k6 can no longer sustain the configured rate and will report dropped iterations, which is itself useful diagnostic information about the system’s actual capacity ceiling.

Choosing the right executor for your question

If the question is “how does the system behave under a fixed user population” (typical for an internal enterprise app with a known user base), use a VU-based executor. If the question is “how does the system behave at a specific request rate regardless of user count” (typical for public-facing APIs and web traffic), use an arrival-rate executor — picking the wrong one answers a different question than the one you actually meant to ask.

Takeaway: k6’s executor model makes the closed-system/open-system distinction a deliberate, explicit configuration choice rather than an implicit assumption buried in how you happened to configure thread/user counts.

Discussions coming soon.

Comments are powered by Giscus (GitHub Discussions). Enable them by configuring GISCUS in src/consts.ts — see giscus.app.