# K6

> 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.

- Skill: `aks-builds/k6` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add aks-builds/k6`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aks-builds/k6/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: aks-builds (https://skillmd.com/u/aks-builds)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aks-builds/k6

---


# 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

```js
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'],            // <1% errors
    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:

```js
thresholds: {
  // Global
  http_req_duration: ['p(95)<300'],
  http_req_failed: ['rate<0.005'],

  // Per-endpoint (using tag)
  'http_req_duration{name:GET /api/products}': ['p(95)<200'],

  // Per-scenario
  'http_req_duration{scenario:checkout}': ['p(99)<1000'],

  // Custom metric
  '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

```js
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)];
  // ... use u.email
}
```

`SharedArray` loads test data once, shared across VUs.

For per-VU initialization (token, account setup), use the `setup()` function:

```js
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

```bash
k6 run script.js                                          # local
k6 run --vus 50 --duration 1m script.js                   # override options
k6 run --out json=results.json script.js                  # JSON output
k6 run --out experimental-prometheus-rw script.js         # Prometheus remote write
k6 cloud script.js                                        # run in k6 Cloud
```

Verify flag names with `k6 run --help` against your installed version.

---

## CI integration

```yaml
- 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:

1. SLO target — p50/p95/p99 latency, RPS, error rate?
2. Traffic shape — smoke, average load, peak, spike, soak?
3. Where does the load run from (local CI runner, k6 Cloud, k6-operator on K8s)?
4. Target environment — perf env, staging, isolated?
5. Are there auth/login steps that should run in `setup()`?
6. How is data parameterized — SharedArray, per-VU generation, fixture file?
7. 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.

