DS DevShelfHub Projects · AI tools
Cheatsheet · Dev tooling

k6: VUs, Scenarios, Thresholds and HTTP Load Testing Reference Guide

By DevShelfHub

VUs, scenarios, stages, thresholds, checks, HTTP, WebSocket, gRPC, browser, extensions — Grafana k6 load testing in JavaScript.

110 items 8 min VUs Scenarios Thresholds

Start hereQuick start · 6 you’ll reach for daily

Runk6 run test.js
CLI vusk6 run --vus 50 --duration 2m
HTTP GEThttp.get(url)
Assertcheck(res, {"200": r => r.status === 200})
SLOthresholds: {"http_req_duration": ["p(95)<300"]}
Exportk6 run --out json=run.json

Target versions · paceVersions

Targets: k6 ≥ 0.50 JS: ES2015+ via goja xk6 (Go)

k6 scripts are JavaScript executed by goja (a Go JS engine) — no Node APIs. Async runtime arrived in 0.50+, but I/O (HTTP, sleep) is synchronous by design. The execution model is scenario → executor → iterations: pick an executor (constant-vus, ramping-vus, constant-arrival-rate…) and k6 schedules iterations of export default function () {}. Use xk6 to compile a custom binary with extensions (Kafka, SQL, browser).

Install · runSetup

bash
# Install — pick one
brew install k6                                # macOS
choco install k6                               # Windows
docker pull grafana/k6                         # container
# Linux: see https://k6.io/docs/get-started/installation/ (apt / dnf repo)

# Hello-world script
cat > test.js <<'EOF'
import http from "k6/http";
import { sleep, check } from "k6";

export const options = { vus: 10, duration: "30s" };

export default function () {
  const res = http.get("https://test.k6.io");
  check(res, { "status is 200": (r) => r.status === 200 });
  sleep(1);
}
EOF

# Run
k6 run test.js                                 # local
k6 run --vus 50 --duration 1m test.js          # CLI overrides
k6 run -e API_URL=https://stg.example.com t.js # env vars
k6 cloud test.js                               # run on Grafana Cloud k6 (paid)

# Output / observability
k6 run --out json=run.json test.js
k6 run --out csv=metrics.csv test.js
k6 run --out experimental-prometheus-rw test.js
k6 run --summary-export=summary.json test.js

# Docker run (mount your script)
docker run --rm -i grafana/k6 run - < test.js

init · setup · default · teardownScript lifecycle

Code outside any exported function is the init stage — runs once per VU. setup()/teardown() run once total. default runs per iteration.

// init code at top of fileRuns once per VU. open() only allowed here.
export function setup() { return seed }Runs once before the test. Return value piped to default.
export default function (data) { ... }The test body — one iteration. data = setup return.
export function teardown(data) { ... }Runs once after the test.
export function handleSummary(data) { return {...} }Customise end-of-run output (JSON, HTML).
__VU / __ITERBuilt-in IDs — current VU and iteration count.
__ENV.XEnv var passed via -e X=... or shell.

export const optionsOptions

vus: 10, duration: "30s"Simplest shape: N VUs, fixed time.
stages: [{ duration, target }, ...]Ramp pattern. Linear interpolation between stages.
iterations: 100Stop after N total iterations (across all VUs).
scenarios: { name: {...} }Preferred Full control — multiple workloads in one run.
thresholds: { metric: ["p(95)<300"] }Pass/fail SLOs (see Thresholds section).
discardResponseBodies: trueHalves memory in high-VU runs. Keep off when you parse JSON.
batch: 20, batchPerHost: 6Max concurrent HTTP requests per http.batch() call.
noConnectionReuse: true / noVUConnectionReuseDisable keep-alive — simulates fresh clients.
httpDebug: "full"Dump every request/response. Debugging only.
tags: { env: "stg" }Global tag on every metric.
summaryTrendStats: ["avg","min","med","p(95)","p(99)","max"]Customise the end-of-run summary.

VU-based vs. arrival-rateExecutors

shared-iterationsFixed total iterations, split across VUs.
per-vu-iterationsEach VU runs n iterations. Use for fairness across VUs.
constant-vusN VUs, run for X duration. The classic load test.
ramping-vusVU count follows stages. Standard ramp-up / soak / ramp-down.
constant-arrival-rateCloser to prod Fix requests/sec, k6 allocates VUs.
ramping-arrival-rateStages for arrival rate (spike tests).
externally-controlledAdjust VUs at runtime via k6 scale.
Arrival-rate executors are the gold standard. VU-based executors throttle naturally as the server slows down — you get fewer real requests, not the load you asked for. Arrival-rate executors keep firing at the target RPS regardless.
javascript
// test.js — multiple scenarios, each with its own executor
import http from "k6/http";

export const options = {
  // Per-scenario tagging propagates to every metric
  scenarios: {
    smoke: {
      executor: "constant-vus",
      vus: 1,
      duration: "1m",
      exec: "browse",                        // function below
      tags: { kind: "smoke" },
    },
    ramp: {
      executor: "ramping-vus",
      startVUs: 0,
      stages: [
        { duration: "30s", target: 50 },     // ramp up
        { duration: "2m",  target: 50 },     // hold
        { duration: "30s", target: 0  },     // ramp down
      ],
      gracefulRampDown: "10s",
      exec: "browse",
    },
    spike: {
      executor: "ramping-arrival-rate",      // req/s, not VUs
      startRate: 10,
      timeUnit: "1s",
      preAllocatedVUs: 100,
      maxVUs: 500,
      stages: [
        { duration: "10s", target: 500 },    // 500 req/s
        { duration: "30s", target: 500 },
        { duration: "10s", target: 10  },
      ],
      exec: "checkout",
    },
  },
  thresholds: {
    "http_req_failed{kind:smoke}": ["rate<0.01"],
    "http_req_duration{scenario:ramp}": ["p(95)<400"],
  },
};

export function browse()   { http.get("https://stg.example.com/items"); }
export function checkout() { http.post("https://stg.example.com/orders", "{}"); }

k6/httpHTTP requests

import http from "k6/http";The HTTP client.
http.get(url, params)GET. params takes headers, tags, timeout, redirects.
http.post(url, body, params)POST. Body: string, object (form-encoded), JSON.stringify, or FormData.
http.put / del / patch / head / optionsOther verbs.
http.request(method, url, body, params)Generic form.
http.batch([{ method, url }, ...])Parallel requests in one call.
res.status / res.body / res.headers / res.timingsResponse fields. timings.duration = total ms.
res.json("path.to.value")Lightweight JSONPath. Cheaper than JSON.parse.
res.html().find("h1").text()jQuery-style selector on HTML responses.
params = { headers, cookies, tags, timeout: "5s", redirects: 0 }Per-request options.
http.cookieJar().set(url, name, value)Default jar is per-VU. new http.CookieJar() for fresh.

check vs. assertChecks, groups, fail

import { check, group, fail, sleep } from "k6";Core test functions.
check(res, { "200": (r) => r.status === 200 })Preferred Record pass/fail — does not stop the iteration.
check(res, {...}, { step: "login" })Tag the check — survives in metrics and thresholds.
fail("explain why")Abort this iteration. The VU starts the next one.
group("login", () => { ... })Tag a block of requests. Metrics carry group tag.
sleep(1) / sleep(Math.random() * 2)Pause this VU. Models think time.
import { sleep } from "k6"; sleep(0.5)Fractional seconds OK.
check() doesn’t fail the test. The run still exits 0. Pair with a threshold like "checks{step:login}": ["rate>0.99"] to fail CI when checks fall below the SLO.

pass/fail SLOsThresholds & metrics

http_req_durationBuilt-in trend — total request time.
http_req_failedRate — fraction of failed requests.
http_reqs / iterations / vus / vus_maxCounters / gauges — volume, parallelism.
data_sent / data_receivedBytes counters.
checksRate — fraction of check()s that passed.
new Counter / Trend / Rate / Gauge (from "k6/metrics")Define custom metrics. Trend = histogram; Rate = pass/fail %.
metric.add(value, { tag: "v" })Push a sample. Tags become threshold filters.
"metric{tag:value}": ["p(95)<300", "p(99)<800"]Multiple thresholds per metric. AND-combined.
{ threshold: "p(95)<300", abortOnFail: true, delayAbortEval: "10s" }Abort the test mid-run when SLO breached.
javascript
// Thresholds: SLOs that fail the run if violated.
import http from "k6/http";
import { Trend, Rate, Counter } from "k6/metrics";

const loginLatency = new Trend("login_latency", true);   // true = time-typed
const errorRate    = new Rate("custom_errors");
const requests     = new Counter("requests_total");

export const options = {
  vus: 20,
  duration: "1m",
  thresholds: {
    // Built-in metrics
    http_req_failed:   ["rate<0.01"],                 // <1% failures overall
    http_req_duration: ["p(95)<300", "p(99)<800"],    // latency SLO

    // Custom metric — fail the run AND abort early
    login_latency: [{ threshold: "p(95)<200", abortOnFail: true, delayAbortEval: "30s" }],

    // Tag-scoped threshold
    "http_req_duration{name:GET /api/items}": ["avg<150"],

    // Counter target
    "requests_total": ["count>1000"],
  },
};

export default function () {
  const t0 = Date.now();
  const res = http.get("https://stg.example.com/login", { tags: { name: "GET /api/login" } });
  loginLatency.add(Date.now() - t0);
  errorRate.add(res.status !== 200);
  requests.add(1);
}

SharedArray · openData & params

const users = new SharedArray("u", () => JSON.parse(open("u.json")))Preferred One shared copy across all VUs.
open("payload.json")Read a file. Init stage only.
JSON.parse(open("data.json"))Without SharedArray each VU gets its own copy — bad at scale.
users[__VU % users.length]Spread data across VUs deterministically.
randomString(8) / randomItem(arr) / uuidv4()From https://jslib.k6.io/k6-utils.
__ENV.BASE_URLRead env vars passed via -e or shell.

WS · gRPC · browserOther protocols

import ws from "k6/ws"; ws.connect(url, params, (socket) => ...)WebSocket client.
socket.on("message", (m) => ...) / .send(...)Event-driven inside the connect callback.
import grpc from "k6/net/grpc"; client.load(["./proto"], "x.proto")gRPC unary + streaming.
client.connect("h:port", { plaintext: true })Open connection. plaintext for dev.
import { browser } from "k6/browser"Browser mode Real Chromium — needs scenarios.x.options.browser.type.
const page = await browser.newPage(); await page.goto("...")Playwright-style API. Mix HTTP load + real-browser flows.

summary · streams · cloudOutput & observability

k6 run --out json=run.jsonStream every metric sample to JSON.
k6 run --out csv=metrics.csvCSV stream — for ad-hoc analysis.
k6 run --out experimental-prometheus-rwPush to a Prometheus remote-write endpoint.
k6 run --out influxdb=http://...Stream to InfluxDB.
k6 run --out cloudGrafana Cloud k6 — needs K6_CLOUD_TOKEN.
--summary-export=summary.jsonFinal aggregate metrics for CI.
--summary-trend-stats="avg,p(95),p(99)"Pick which stats appear in the end-of-run table.
export function handleSummary(data) { return { "out.html": htmlReport(data) }; }Emit any artefact (HTML, JUnit) from script.

login → browse → checkoutEnd-to-end · full flow

Reads user pool from a fixture, ramps to 25 VUs, hits three endpoints per iteration, tags requests by step, and fails CI if the latency SLO or check-pass rate drops.

javascript
// e2e.js — login → list → checkout, with grouping, checks, thresholds
import http from "k6/http";
import { group, check, sleep, fail } from "k6";
import { SharedArray } from "k6/data";

const users = new SharedArray("users", () => JSON.parse(open("./users.json"))); // one shared copy

export const options = {
  stages: [
    { duration: "30s", target: 25 },
    { duration: "2m",  target: 25 },
    { duration: "20s", target: 0  },
  ],
  thresholds: {
    http_req_failed:   ["rate<0.01"],
    http_req_duration: ["p(95)<500"],
    "checks{step:login}": ["rate>0.99"],
  },
};

const BASE = __ENV.API_URL || "https://stg.example.com";

export default function () {
  const u = users[__VU % users.length];
  let token;

  group("login", () => {
    const res = http.post(`${BASE}/api/login`, JSON.stringify(u), {
      headers: { "Content-Type": "application/json" },
      tags:    { step: "login" },
    });
    if (!check(res, { "200": (r) => r.status === 200 }, { step: "login" })) fail("login failed");
    token = res.json("token");
  });

  const auth = { headers: { Authorization: `Bearer ${token}` } };

  group("browse", () => {
    const r = http.get(`${BASE}/api/items`, auth);
    check(r, { "list ok": (r) => r.status === 200 && r.json().length > 0 });
  });

  group("checkout", () => {
    const r = http.post(`${BASE}/api/orders`, "{}", auth);
    check(r, { "created": (r) => r.status === 201 });
  });

  sleep(1);
}

Best practiceGood to know

Prefer arrival-rate executors for prod-shaped load. VU-based runs throttle when the server gets slow — you measure how slow your test driver is, not the server. Arrival-rate keeps firing at the target RPS.
Thresholds are the pass/fail gate — not check(). Define your SLO once in thresholds; CI fails when reality drifts. Checks tell you which assertion broke.
SharedArray for any test data > 1 KB. Without it, every VU duplicates the payload — memory explodes at 1k+ VUs and you OOM the runner before the server feels anything.

Common trapsWatch out for

k6 is not Node. No fs, no net, no npm. Only k6/* modules and ES2015+. open() only in the init stage.
check() failing doesn’t fail the run. The exit code is 0 unless a threshold is breached. Always set a checks{...} threshold for the assertions that matter.
Defaults reuse connections. Realistic for "warm" load, misleading for "1000 fresh clients per second" tests. Set noConnectionReuse: true or use cloud load generators.

Go deeperSee also

k6 FAQ

What is k6 and who makes it?

k6 is an open-source load testing tool built by Grafana Labs. Tests are written in JavaScript or TypeScript, executed by a Go runtime for high concurrency and low overhead. k6 can simulate thousands of virtual users from a single machine or scale out via Grafana Cloud k6.

What is a virtual user (VU) in k6?

A virtual user is an isolated execution context that runs your test script in a loop for the duration of the test. Each VU maintains its own cookies, headers, and connection state. Use --vus 50 --duration 2m from the CLI or configure the vus and duration options in the exported options object.

How do I define load ramp-up stages in k6?

Set the stages array in the exported options object: stages: [{ duration: "1m", target: 100 }, { duration: "3m", target: 100 }, { duration: "1m", target: 0 }]. Each stage ramps from the current VU count to the target over the specified duration. Stages use the ramping-vus executor under the hood.

What are k6 thresholds?

Thresholds are pass/fail SLO assertions on built-in or custom metrics. Define them in options.thresholds: { "http_req_duration": ["p(95)<300", "p(99)<600"], "http_req_failed": ["rate<0.01"] }. k6 exits with a non-zero code if any threshold is breached, making it easy to fail a CI pipeline.

Can k6 test WebSockets and browser interactions?

Yes. k6 has a built-in WebSocket API (k6/ws) for protocol-level testing and a browser module (k6/browser) that drives a Chromium instance for real browser testing with metrics like LCP and CLS. Browser tests run with k6 run --browser.enabled test.js and mix easily with HTTP scenarios.

How do I export k6 results to Grafana?

Use k6 run --out influxdb=http://localhost:8086/k6 to stream metrics to InfluxDB, then visualise in Grafana with the k6 dashboard template. Alternatively, use Grafana Cloud k6 (k6 cloud run test.js) to automatically store results, compare runs, and share dashboards without self-hosting.