Skip to main content

Scenarios

Scenarios provide fine-grained control over how k6 executes your test. They allow you to model complex user behavior patterns, run different workloads in parallel, and precisely control VU execution.

Why Use Scenarios?

Scenarios are more powerful than simple vus, duration, or stages options:
  • Multiple workflows: Run different test functions simultaneously
  • Different load patterns: Combine constant load, ramping, and spike patterns
  • Precise VU control: Control exactly how VUs execute iterations
  • Per-scenario configuration: Set different options for each scenario
  • Advanced metrics: Get per-scenario metrics and tags

Basic Scenario Structure

import http from 'k6/http';

export let options = {
  scenarios: {
    my_scenario: {                    // Scenario name
      executor: 'constant-vus',       // Executor type
      vus: 10,                        // Executor-specific options
      duration: '30s',
    },
  },
};

export default function() {
  http.get('https://quickpizza.grafana.com');
}

Executors

Executors define how VUs are scheduled and iterations are executed. k6 provides several executor types:

Constant VUs

Maintains a constant number of VUs for a duration.
export let options = {
  scenarios: {
    constant_load: {
      executor: 'constant-vus',
      vus: 10,
      duration: '1m',
    },
  },
};
Use case: Steady-state load testing

Ramping VUs

Gradually ramps VUs up and down.
export let options = {
  scenarios: {
    ramping_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 10 },
        { duration: '1m', target: 10 },
        { duration: '30s', target: 0 },
      ],
      gracefulRampDown: '30s',
    },
  },
};
Use case: Gradually increasing load, soak testing

Constant Arrival Rate

Starts iterations at a constant rate, adding VUs as needed.
export let options = {
  scenarios: {
    constant_rate: {
      executor: 'constant-arrival-rate',
      rate: 30,              // 30 iterations per timeUnit
      timeUnit: '1s',        // per second
      duration: '1m',
      preAllocatedVUs: 10,   // Pre-allocated VUs
      maxVUs: 50,            // Maximum VUs to allocate
    },
  },
};
Use case: Testing system under constant request rate, measuring latency under load
Constant arrival rate focuses on iteration starts per time unit, not concurrent VUs. k6 will add VUs as needed to maintain the rate.

Ramping Arrival Rate

Gradually increases or decreases the iteration start rate.
export let options = {
  scenarios: {
    ramping_rate: {
      executor: 'ramping-arrival-rate',
      startRate: 10,
      timeUnit: '1s',
      preAllocatedVUs: 20,
      maxVUs: 100,
      stages: [
        { duration: '30s', target: 10 },   // Start at 10 iters/s
        { duration: '1m', target: 50 },    // Ramp to 50 iters/s
        { duration: '30s', target: 10 },   // Ramp down to 10 iters/s
      ],
    },
  },
};
Use case: Gradually increasing throughput testing

Shared Iterations

Shares a fixed number of iterations among VUs.
export let options = {
  scenarios: {
    shared_iters: {
      executor: 'shared-iterations',
      vus: 10,
      iterations: 100,       // Total iterations shared across VUs
      maxDuration: '30s',
    },
  },
};
Use case: Completing a specific number of test iterations as quickly as possible

Per-VU Iterations

Each VU executes a specific number of iterations.
export let options = {
  scenarios: {
    per_vu_iters: {
      executor: 'per-vu-iterations',
      vus: 10,
      iterations: 10,        // Each VU runs 10 iterations
      maxDuration: '30s',
    },
  },
};
Use case: Testing with a precise total number of iterations (vus × iterations)

Externally Controlled

Control execution externally via k6’s REST API.
export let options = {
  scenarios: {
    external: {
      executor: 'externally-controlled',
      vus: 10,
      maxVUs: 50,
      duration: '5m',
    },
  },
};
Use case: Dynamic control of VUs during test execution

Multiple Scenarios

Run different test functions with different load patterns:
import { browser } from 'k6/browser';

export const options = {
  scenarios: {
    messages: {
      executor: 'constant-vus',
      exec: 'messages',         // Function to execute
      vus: 1,
      duration: '2s',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
    news: {
      executor: 'per-vu-iterations',
      exec: 'news',             // Different function
      vus: 1,
      iterations: 2,
      maxDuration: '5s',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
  thresholds: {
    browser_web_vital_fcp: ['max < 5000'],
    checks: ["rate==1.0"]
  }
}

export async function messages() {
  const page = await browser.newPage();
  try {
    await page.goto('https://quickpizza.grafana.com/my_messages.php', { 
      waitUntil: 'networkidle' 
    });
  } finally {
    await page.close();
  }
}

export async function news() {
  const page = await browser.newPage();
  try {
    await page.goto('https://quickpizza.grafana.com/news.php', { 
      waitUntil: 'networkidle' 
    });
  } finally {
    await page.close();
  }
}

Scenario Configuration Options

Common Options

All scenarios support these options:
export let options = {
  scenarios: {
    my_scenario: {
      executor: 'constant-vus',
      
      // Execution control
      startTime: '10s',              // Delay start by 10s
      gracefulStop: '30s',           // Time to finish iterations after duration
      
      // Function to execute
      exec: 'myFunction',            // Default: 'default'
      
      // Environment variables
      env: {
        MY_VAR: 'value',
      },
      
      // Tags for this scenario's metrics
      tags: {
        scenario_type: 'api',
        team: 'backend',
      },
    },
  },
};

export function myFunction() {
  // Custom test logic
}

Staggered Start Times

Start scenarios at different times:
export let options = {
  scenarios: {
    warmup: {
      executor: 'constant-vus',
      vus: 5,
      duration: '1m',
      startTime: '0s',             // Start immediately
    },
    main_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },
      ],
      startTime: '1m',             // Start after warmup
    },
  },
};

Scenario Tags and Metrics

Each scenario automatically gets tagged:
import http from 'k6/http';
import { check } from 'k6';

export let options = {
  scenarios: {
    api_test: {
      executor: 'constant-vus',
      vus: 10,
      duration: '30s',
      tags: {
        test_type: 'api',
        version: 'v2',
      },
    },
  },
  thresholds: {
    // Threshold for specific scenario
    'http_req_duration{scenario:api_test}': ['p(95)<500'],
  },
};

export default function() {
  http.get('https://quickpizza.grafana.com/api/menu');
}

Graceful Stop

Control how iterations finish at the end:
export let options = {
  scenarios: {
    my_scenario: {
      executor: 'constant-vus',
      vus: 10,
      duration: '1m',
      gracefulStop: '30s',    // Wait up to 30s for iterations to finish
    },
  },
};
If iterations don’t complete within the gracefulStop period, they are forcefully terminated.

Scenario Examples

export let options = {
  scenarios: {
    spike: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '10s', target: 10 },   // Normal load
        { duration: '10s', target: 100 },  // Spike!
        { duration: '10s', target: 10 },   // Back to normal
      ],
      gracefulRampDown: '10s',
    },
  },
};

Executor Comparison

ExecutorControl TypeUse Case
constant-vusVU countSteady-state load
ramping-vusVU countGradual load changes
constant-arrival-rateIteration rateFixed throughput
ramping-arrival-rateIteration rateGradual throughput changes
shared-iterationsTotal iterationsQuick completion of fixed work
per-vu-iterationsPer-VU iterationsPrecise iteration count
externally-controlledExternal APIDynamic control

Best Practices

  1. Use arrival rate for throughput testing - When you care about requests/second, not VU count
  2. Set appropriate maxVUs - For arrival rate executors, allocate enough VUs to maintain the rate
  3. Add graceful stop time - Give long-running iterations time to complete
  4. Use scenario tags - Tag scenarios for easier metric filtering
  5. Stagger scenario starts - Avoid overwhelming the system at test start
  6. Name scenarios descriptively - Use clear names like api_load, browser_flow, etc.

Scenario Metrics

Filter metrics by scenario:
export let options = {
  scenarios: {
    api_test: {
      executor: 'constant-vus',
      vus: 10,
      duration: '1m',
    },
  },
  thresholds: {
    'http_req_duration{scenario:api_test}': ['p(95)<500'],
    'http_req_failed{scenario:api_test}': ['rate<0.01'],
    'iterations{scenario:api_test}': ['count>100'],
  },
};

Next Steps

Build docs developers (and LLMs) love