A single export default function with one VU count and one stage pattern cannot model mixed workloads. Real systems serve multiple user types simultaneously: some users browse, some search, some complete purchases. Scenarios let you run distinct user flows — each with its own VU count, executor, and timing — in a single test run.
Three scenarios running in parallel
Step 1 of 3
browsers (50 VUs)
ramping-vus executor. Calls browseProducts(). 50 VUs ramping up over 2 minutes, holding for 5 minutes. Represents users browsing the product catalogue — high volume, simple reads.
All three scenarios hit the same backend simultaneously. The system must handle mixed load — not just one kind of traffic.
Defining scenarios
Scenarios are declared in options.scenarios. Each scenario names a function to execute:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
browsers: {
executor: 'ramping-vus',
exec: 'browseProducts',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
tags: { scenario: 'browse' },
},
checkout: {
executor: 'constant-vus',
exec: 'checkoutFlow',
vus: 10,
duration: '9m',
startTime: '30s',
tags: { scenario: 'checkout' },
},
api_load: {
executor: 'constant-arrival-rate',
exec: 'apiCall',
rate: 100,
timeUnit: '1s',
duration: '9m',
preAllocatedVUs: 50,
maxVUs: 200,
tags: { scenario: 'api' },
},
},
};
export function browseProducts() {
const res = http.get('https://shop.example.com/products', {
tags: { name: 'ListProducts' },
});
check(res, { 'products listed': (r) => r.status === 200 });
sleep(2);
}
export function checkoutFlow() {
const cartRes = http.post('https://shop.example.com/cart/items',
JSON.stringify({ productId: Math.ceil(Math.random() * 100) }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'AddToCart' } }
);
check(cartRes, { 'added to cart': (r) => r.status === 201 });
const orderRes = http.post('https://shop.example.com/orders',
JSON.stringify({ paymentMethod: 'card_test' }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'PlaceOrder' } }
);
check(orderRes, { 'order placed': (r) => r.status === 201 });
sleep(5);
}
export function apiCall() {
http.get('https://api.example.com/products/1', {
tags: { name: 'ApiGetProduct' },
});
}Each exported function is independent. browseProducts, checkoutFlow, and apiCall run concurrently — browseProducts VUs never call checkoutFlow logic and vice versa.
Scenario tags for metric filtering
The tags: { scenario: 'browse' } on each scenario automatically tags all requests made within that scenario. Combined with request-level tags, this gives you two dimensions of filtering in the output and Grafana:
http_req_duration{scenario:browse,name:ListProducts}...: p(95)=180ms
http_req_duration{scenario:checkout,name:AddToCart}.....: p(95)=420ms
http_req_duration{scenario:checkout,name:PlaceOrder}....: p(95)=680ms
http_req_duration{scenario:api,name:ApiGetProduct}......: p(95)=95msAdd thresholds per scenario using this tag syntax:
thresholds: {
'http_req_duration{scenario:checkout}': ['p(95)<1000'],
'http_req_duration{scenario:browse}': ['p(95)<300'],
'http_req_duration{scenario:api}': ['p(95)<200'],
},Sequential scenarios with startTime
By default all scenarios start at t=0. Use startTime to sequence them:
scenarios: {
warmup: {
executor: 'constant-vus',
exec: 'warmupCaches',
vus: 5,
duration: '2m',
startTime: '0s',
},
main_load: {
executor: 'ramping-vus',
exec: 'mainTest',
startVUs: 0,
stages: [
{ duration: '3m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 },
],
startTime: '2m', // starts after warmup completes
},
},startTime is an offset from the beginning of the test. Set it to the duration of the preceding scenario to create strictly sequential execution.
setup() and teardown() with scenarios
setup() and teardown() still run once each, before and after all scenarios. The data returned by setup() is passed to every function called by every scenario:
export function setup() {
// Log in once — token available to all scenario functions
const res = http.post('https://api.example.com/auth/login', JSON.stringify({
email: 'loadtest@example.com', password: 'TestPass@1234',
}), { headers: { 'Content-Type': 'application/json' } });
return { token: res.json('token') };
}
export function browseProducts(data) {
http.get('https://shop.example.com/products', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
export function checkoutFlow(data) {
http.post('https://shop.example.com/orders',
JSON.stringify({ productId: 1 }),
{ headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${data.token}`,
}
}
);
}⚠️ Common mistakes
- Not exporting scenario functions. Scenario functions must be
export function browseProducts()— not justfunction browseProducts(). K6 can only findexectargets that are exported. An unexported function silently causes the scenario to fail to start. - Expecting
default()to run when using scenarios. Whenoptions.scenariosis defined, K6 only runs the functions named in each scenario'sexecfield.export default functionis ignored unless a scenario explicitly references it withexec: 'default'. - No
startTimefor sequential scenarios. WithoutstartTime, all scenarios start simultaneously. If yoursetup_datascenario is supposed to run beforemain_test, they will overlap unless you setstartTimeonmain_test. - Missing
maxVUson arrival-rate executors.constant-arrival-ratespawns new VUs to maintain the rate if existing VUs are too slow. WithoutmaxVUs, it can spawn unlimited VUs. Always cap withmaxVUsto prevent runaway resource consumption.
🎯 Practice task
Build a multi-scenario test against a public API. 40 minutes.
Use https://jsonplaceholder.typicode.com.
- Write a script with two scenarios:
readers:constant-vus,exec: 'readPosts', 5 VUs, 2-minute duration.writers:constant-vus,exec: 'createPost', 2 VUs, 90-second duration,startTime: '15s'.
- Add scenario tags:
tags: { scenario: 'read' }andtags: { scenario: 'write' }. - Write
export function readPosts():GET /posts(taggedListPosts), check status 200,sleep(1). - Write
export function createPost():POST /postswithJSON.stringify({ title: 'K6 Test', body: 'load test', userId: 1 }), check status 201,sleep(2). - Add thresholds:
'http_req_duration{scenario:read}': ['p(95)<400']and'http_req_duration{scenario:write}': ['p(95)<800']. - Run the test. Verify in the output that metric rows appear for both scenarios. Note the
startTimedelay — confirm thewritersscenario metrics only appear after 15 seconds of the test. - Add
export function setup()that makes one request and returns{ startedAt: Date.now() }. Modify both exported functions to acceptdataand logdata.startedAt. Confirm setup runs once.