Skip to main content

k6 Concepts

Table of Contents

Why a mental model first​

k6 scripts look like plain JavaScript, but they don't run like a normal Node program: the same file gets loaded once per VU, a few functions run at fixed points regardless of how many VUs there are, and load shape lives in a config object rather than in a loop you write yourself. Knowing which part of the script runs when — and how many times — is what makes the rest of this track's scripts readable instead of magic.

How: the lifecycle​

k6 lifecycle: init code runs once per VU, setup() runs once and its return value is copied into every VU and into teardown(), each VU loops the default function independently, teardown() runs once at the end

StageRunsTypically used for
Init codeOnce per VU, before that VU's iterations startimports, SharedArray test data, constants — nothing that touches the network
setup()Once total, before any VU iterateslogging in as owner@playground.local, resetting the playground, computing data to hand to every VU
default functionOnce per iteration, for every VUthe actual business flow — requests, checks
teardown()Once total, after every VU has finishedcleanup, or asserting something about the final state

setup() and teardown() are optional — 01-first-test.js uses neither. 03-order-flow.js in the sample uses setup() to reset the playground once before the whole run.

How: VU vs. iteration​

A VU (virtual user) is one of the parallel "workers" k6 spins up to run your default function — think of it as one simulated user's execution thread. An iteration is a single run of the default function, start to end, by one VU. options.vus: 1, duration: '10s' means one VU looping its default function for 10 seconds — however many iterations fit in that time, which is exactly what 01-first-test.js reports: 0/1 VUs, 10 complete ... iterations (the 0 is VUs still active at that instant — the final summary line prints after the one VU has finished).

How: options vs. CLI vs. -e​

Three ways to configure a run, in order of precedence (highest wins):

  1. CLI flags — k6 run --vus 10 --duration 30s script.js
  2. Environment variables — K6_VUS=10 K6_DURATION=30s k6 run script.js (built-in options only; these map to the same names as the CLI flags, prefixed K6_)
  3. The script's options object — export const options = { vus: 1, duration: '10s' }, the fallback when nothing else is set

-e NAME=value is a different mechanism: it does not set a built-in option, it sets a custom variable your script reads yourself via __ENV.NAME. That's how every script in k6/sample/ reads BASE_URL, USERS, SCALE and the rest — see k6/README.md for the full list.

How: __ENV, __VU, __ITER​

Three globals k6 injects into every script:

  • __ENV — an object of everything passed with -e, plus the process environment. __ENV.BASE_URL || 'http://127.0.0.1:8080' is the pattern used throughout the sample (see lib/config.js).
  • __VU — the current VU's number, starting at 1. lib/data.js uses (vu - 1) % customers.length to give each VU a customer login, wrapping around once VUs outnumber rows.
  • __ITER — the current iteration number for this VU, starting at 0. Useful for varying behaviour across a VU's own iterations (e.g. only doing something expensive on __ITER === 0).

The first script, in full​

k6/sample/01-first-test.js:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { API } from './lib/config.js';

export const options = {
vus: 1,
duration: '10s',
thresholds: {
checks: ['rate==1'], // every check must pass
http_req_duration: ['p(95)<500'],
},
};

export default function () {
const res = http.get(`${API}/menu`);
check(res, {
'status is 200': (r) => r.status === 200,
'menu has pizzas': (r) => r.json('pizzas').length > 0,
});
sleep(1);
}

One VU, one default function, no setup() or teardown() — the smallest complete k6 script. check() records pass/fail without stopping the run; the thresholds block is what actually fails the test (and the process exit code) if either condition is not met.

JMeter → k6 cheat sheet​

JMeterk6
Thread Groupa scenario / executor in options.scenarios (or vus/stages for the simple case)
HTTP Request samplerhttp.get(...) / http.post(...)
Assertioncheck(res, { ... })
JSON Extractorres.json('path')
CSV Data Set ConfigSharedArray (see lib/data.js)
Timersleep(seconds)
Transaction Controllergroup('name', () => { ... })
Listener--out <destination> while running, or the end-of-test summary
User Defined Variables / -J properties-e KEY=value, read back via __ENV.KEY

Tips​

  • k6 is not Node. Scripts run on Sobek, Grafana's fork of the goja JavaScript engine, not on Node or V8. It implements the JS spec, not Node's runtime APIs — so an npm package that needs fs, native modules, or other Node-only APIs will not load in a k6 script, no matter how small it looks. Stick to the k6/* modules and plain JS; if you need an npm-style package, check whether Grafana ships an equivalent k6/x/... extension first.