Start hereQuick start · 6 you’ll reach for daily
Target versions · paceVersions
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
# 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 file | Runs 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 / __ITER | Built-in IDs — current VU and iteration count. |
| __ENV.X | Env 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: 100 | Stop 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: true | Halves memory in high-VU runs. Keep off when you parse JSON. |
| batch: 20, batchPerHost: 6 | Max concurrent HTTP requests per http.batch() call. |
| noConnectionReuse: true / noVUConnectionReuse | Disable 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-iterations | Fixed total iterations, split across VUs. |
| per-vu-iterations | Each VU runs n iterations. Use for fairness across VUs. |
| constant-vus | N VUs, run for X duration. The classic load test. |
| ramping-vus | VU count follows stages. Standard ramp-up / soak / ramp-down. |
| constant-arrival-rate | Closer to prod Fix requests/sec, k6 allocates VUs. |
| ramping-arrival-rate | Stages for arrival rate (spike tests). |
| externally-controlled | Adjust VUs at runtime via k6 scale. |
// 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 / options | Other 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.timings | Response 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_duration | Built-in trend — total request time. |
| http_req_failed | Rate — fraction of failed requests. |
| http_reqs / iterations / vus / vus_max | Counters / gauges — volume, parallelism. |
| data_sent / data_received | Bytes counters. |
| checks | Rate — 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. |
// 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_URL | Read 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.json | Stream every metric sample to JSON. |
| k6 run --out csv=metrics.csv | CSV stream — for ad-hoc analysis. |
| k6 run --out experimental-prometheus-rw | Push to a Prometheus remote-write endpoint. |
| k6 run --out influxdb=http://... | Stream to InfluxDB. |
| k6 run --out cloud | Grafana Cloud k6 — needs K6_CLOUD_TOKEN. |
| --summary-export=summary.json | Final 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.
// 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
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
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.
noConnectionReuse: true or use cloud load generators.