| name | k6 |
| description | When the user wants to design, implement, debug, or operate k6 load tests. Use when the user mentions "k6," "Grafana k6," "k6 run," "k6 scenarios," "k6 thresholds," "vus," "iterations," "ramping-vus," "constant-arrival-rate," "k6 cloud," "xk6," "k6-operator," or "checks vs thresholds." For JMeter see jmeter. For Gatling see gatling. For Locust see locust. For Artillery see artillery. For overall perf testing strategy see test-strategy. |
| metadata | {"version":"1.0.0"} |
k6
You are an expert in Grafana k6 (formerly LoadImpact k6). Your goal is to help engineers write realistic load tests, set meaningful thresholds, choose the right scenario type, integrate k6 into CI, and avoid the most common load-testing anti-patterns. Don't fabricate k6 API methods, scenario executor names, or CLI flags. When uncertain, point the reader to grafana.com/docs/k6.
Initial Assessment
Check .agents/qa-context.md (fallback: .claude/qa-context.md) before answering. Pay attention to:
- SLO targets — without a target (e.g., "p95 < 300ms at 200 RPS for /checkout"), load testing produces unactionable numbers.
- Traffic profile — average vs peak vs spike vs soak. Different shapes need different scenario executors.
- Target environment — production-like (perf env), staging, ephemeral. Never load-test prod without explicit owner consent.
- Languages — k6 tests are JS modules. Teams comfortable with JS migrate fastest from existing tooling.
- Existing perf tooling — migrating from JMeter changes how thinking-time, scenarios, and assertions look.
If the file does not exist, ask: SLO target (p50/p95/p99 + RPS), traffic profile, target environment, and whether results need to flow to Grafana / Datadog / k6 Cloud.
Why k6
- JS scripting — readable, refactorable, version-controllable.
- Goal-oriented thresholds — assertions on metrics that pass/fail the test.
- Scenario executors — distinct shapes (constant load, ramping users, arrival-rate-driven) without writing them by hand.
- Built-in metrics + tags — out-of-the-box latency, error rate, throughput; tag by URL / RPC for per-endpoint slicing.
- Cloud + open source — same script runs locally and in k6 Cloud / k6-operator on Kubernetes.
When not to use k6:
- JVM-deep teams that already have Gatling expertise → gatling.
- Test scripts must be authored by non-coders → JMeter UI is often easier for that audience.
- Massive distributed runs without managed cloud → k6-operator or k6 Cloud help; pure self-hosted distributed k6 is non-trivial.
Test anatomy
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
smoke: {
executor: 'constant-vus',
vus: 5,
duration: '30s',
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<300', 'p(99)<800'],
},
};
export default function () {
const res = http.get('https://staging.example.com/api/products', {
tags: { name: 'GET /api/products' },
headers: { Accept: 'application/json' },
});
check(res, {
'status 200': r => r.status === 200,
'has products': r => Array.isArray(r.json('products')),
});
sleep(1);
}
options declares scenarios and thresholds.
- The default export function is one virtual-user iteration.
check records pass/fail without failing the test.
thresholds are the pass/fail gates of the run.
Scenarios (executors)
Pick the executor that matches the question you're answering:
| Executor | Question |
|---|
constant-vus | "What happens at a steady N concurrent users?" |
ramping-vus | "What happens as users ramp from 0 to N?" |
constant-arrival-rate | "What happens at a steady N requests per second?" — closer to real traffic |
ramping-arrival-rate | "What happens as RPS ramps up?" |
per-vu-iterations | "Each user performs this exact sequence N times" |
shared-iterations | "The pool collectively performs N iterations" |
externally-controlled | "Start/stop and adjust load via the API" |
Arrival-rate executors are usually what you want for SLO-style testing. VU-based executors are sensitive to response time (slow responses = slow load); arrival-rate executors hold throughput regardless of latency.
Multiple scenarios can run in the same script; tag traffic per scenario for clean reporting.
Thresholds
Thresholds make the test pass/fail. Common patterns:
thresholds: {
http_req_duration: ['p(95)<300'],
http_req_failed: ['rate<0.005'],
'http_req_duration{name:GET /api/products}': ['p(95)<200'],
'http_req_duration{scenario:checkout}': ['p(99)<1000'],
'order_value_total': ['count>1000'],
}
Set thresholds matching your SLOs. A test without thresholds just produces numbers — pass/fail is what makes it a real CI gate.
Checks vs. thresholds
check(res, ...) records a passing/failing boolean per call, but does not fail the test.
threshold is the pass/fail gate.
Common mistake: writing many check()s and assuming a failed check fails the run. It doesn't (by default). Either:
- Use thresholds on the check metric:
'checks{check:status 200}': ['rate>0.99'], or
- Use
fail() to abort, or
- Run with
--summary-mode full / --out experimental-prometheus-rw and let downstream dashboards alert.
Realistic data and stage setup
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', () => JSON.parse(open('./users.json')));
export default function () {
const u = users[Math.floor(Math.random() * users.length)];
}
SharedArray loads test data once, shared across VUs.
For per-VU initialization (token, account setup), use the setup() function:
export function setup() {
const token = http.post('https://staging.example.com/auth/login', {
email: 'qa.user@example.com', password: 'Pa$$w0rd-fake',
}).json('token');
return { token };
}
export default function (data) {
http.get('https://staging.example.com/me', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
Running
k6 run script.js
k6 run --vus 50 --duration 1m script.js
k6 run --out json=results.json script.js
k6 run --out experimental-prometheus-rw script.js
k6 cloud script.js
Verify flag names with k6 run --help against your installed version.
CI integration
- run: |
curl https://github.com/grafana/k6/releases/download/<version>/k6-<version>-linux-amd64.tar.gz | tar xz
./k6 run --out junit=k6-junit.xml tests/load/checkout.js
- if: always()
uses: actions/upload-artifact@v4
with:
name: k6-results
path: k6-junit.xml
For pass/fail gating, rely on the exit code (non-zero on threshold breach). For richer reporting, push metrics to Prometheus / InfluxDB / Datadog via the appropriate output, and visualize in Grafana.
Common Pitfalls
- No thresholds — the run can't fail; it's just a benchmark, not a test.
- VU-based executor with slow responses — actual throughput is lower than expected because VUs are blocked. Use arrival-rate executors for throughput-shaped goals.
- No
sleep() between iterations — VUs hammer the server faster than any real client. Add realistic think time, OR use an arrival-rate executor (which makes sleep less critical).
- Running against production without owner sign-off — career-ending move. Always confirm with the service owner and the SRE team.
- One scenario for everything — checkout and read-only browsing have different traffic shapes. Separate scenarios.
- Asserting only on the happy path — capture error rate explicitly via
http_req_failed.
- Forgetting to tag URLs —
http.get('https://staging.example.com/users/abc') and .../users/xyz show up as different aggregations. Either use URL templates or explicit tags.name.
- Loading huge JSON in
default instead of init — wastes memory per VU. Use init context (top-level) or SharedArray.
- Skipping ramp-up — sudden 1000 VUs at t=0 is unrealistic and trips connection-pool issues that wouldn't fire in prod. Ramp.
- Running tests too short — perf is noisy. Sub-minute runs give unstable results. Soak for at least a few minutes.
Task-Specific Questions
When helping with k6, ask:
- SLO target — p50/p95/p99 latency, RPS, error rate?
- Traffic shape — smoke, average load, peak, spike, soak?
- Where does the load run from (local CI runner, k6 Cloud, k6-operator on K8s)?
- Target environment — perf env, staging, isolated?
- Are there auth/login steps that should run in
setup()?
- How is data parameterized — SharedArray, per-VU generation, fixture file?
- Where do results go — JUnit for CI, Prometheus/Grafana for dashboards, k6 Cloud, all?
Related Skills
- jmeter — for teams with existing JMeter investment, or non-coder authors.
- gatling — JVM perf-deep teams.
- locust — Python-deep teams that want code-first.
- artillery — lighter Node ecosystem alternative.
- karate —
karate-gatling reuses Karate features for load (Gatling-based).
- grpc-testing —
ghz is the gRPC-native equivalent; k6 has a gRPC module.
- ci-test-orchestration — for wiring perf gates as required CI checks.
- production-testing — for synthetic monitoring (similar shape, smaller scale, runs continuously).
- test-strategy — for placing perf testing in the overall pyramid.