Skip to main content
Technology & EngineeringTesting Services220 lines

K6

k6: load testing, performance testing, stress testing, soak testing, thresholds, checks, scenarios, browser testing, CI integration

Quick Summary9 lines
You are an expert in using k6 for load testing, performance testing, and reliability validation of APIs and web applications.

## Key Points

- Define thresholds for every test so CI pipelines can automatically pass or fail based on performance budgets; never rely on manually inspecting output after each run.
- Use the `setup()` function for one-time operations like authentication or test data creation, and `teardown()` for cleanup; these run outside the VU loop and do not inflate metrics.
- Start with a realistic baseline (the `ramping-vus` executor) before moving to stress or spike tests; establish what "normal" looks like so regressions are detectable.
skilldb get testing-services-skills/K6Full skill: 220 lines
Paste into your CLAUDE.md or agent config

k6 — Testing

You are an expert in using k6 for load testing, performance testing, and reliability validation of APIs and web applications.

Core Philosophy

Overview

k6 is an open-source load testing tool built by Grafana Labs. Tests are written in JavaScript/TypeScript and executed by a high-performance Go runtime, so a single machine can simulate thousands of concurrent virtual users without the overhead of real browser instances. k6 supports HTTP, WebSocket, gRPC, and (via the browser module) real Chromium-based browser interactions. It is designed to be developer-centric: tests live in version control, run in CI pipelines, and produce machine-readable output for dashboards and automated pass/fail decisions via thresholds.

Setup & Configuration

Installation

# macOS
brew install k6

# Windows (winget)
winget install k6 --source winget

# Docker
docker run --rm -i grafana/k6 run - <script.js

# npm wrapper (for JS/TS projects)
npm install -D @grafana/k6

Basic test structure

// tests/load/api-load.js
import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  // Ramp up to 50 users over 30s, hold for 1m, ramp down
  stages: [
    { duration: "30s", target: 50 },
    { duration: "1m", target: 50 },
    { duration: "10s", target: 0 },
  ],
  // Automated pass/fail criteria
  thresholds: {
    http_req_duration: ["p(95)<300"], // 95th percentile under 300ms
    http_req_failed: ["rate<0.01"],   // less than 1% errors
  },
};

export default function () {
  const res = http.get("https://api.example.com/users");

  check(res, {
    "status is 200": (r) => r.status === 200,
    "response time < 500ms": (r) => r.timings.duration < 500,
    "body has users array": (r) => JSON.parse(r.body).users.length > 0,
  });

  sleep(1); // simulate think time between requests
}

Running tests

# Run locally
k6 run tests/load/api-load.js

# Run with environment variables
k6 run -e BASE_URL=https://staging.example.com tests/load/api-load.js

# Output results to JSON for post-processing
k6 run --out json=results.json tests/load/api-load.js

# Stream results to Grafana Cloud k6
k6 cloud run tests/load/api-load.js

Core Patterns

Scenario-based testing (multiple workloads)

// tests/load/mixed-workload.js
import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  scenarios: {
    browse_api: {
      executor: "ramping-vus",
      startVUs: 0,
      stages: [
        { duration: "1m", target: 100 },
        { duration: "3m", target: 100 },
        { duration: "30s", target: 0 },
      ],
      exec: "browseEndpoint",
    },
    create_orders: {
      executor: "constant-arrival-rate",
      rate: 10,           // 10 iterations per timeUnit
      timeUnit: "1s",     // = 10 RPS
      duration: "4m",
      preAllocatedVUs: 20,
      maxVUs: 50,
      exec: "createOrder",
    },
  },
  thresholds: {
    "http_req_duration{scenario:browse_api}": ["p(95)<200"],
    "http_req_duration{scenario:create_orders}": ["p(95)<500"],
  },
};

const BASE_URL = __ENV.BASE_URL || "https://api.example.com";

export function browseEndpoint() {
  const res = http.get(`${BASE_URL}/products`);
  check(res, { "browse 200": (r) => r.status === 200 });
  sleep(Math.random() * 2);
}

export function createOrder() {
  const payload = JSON.stringify({
    productId: "prod-001",
    quantity: Math.ceil(Math.random() * 5),
  });
  const params = { headers: { "Content-Type": "application/json" } };
  const res = http.post(`${BASE_URL}/orders`, payload, params);
  check(res, { "order created": (r) => r.status === 201 });
}

Authentication and headers

// tests/load/authenticated.js
import http from "k6/http";
import { check } from "k6";

export function setup() {
  // Runs once before all VUs; return value is passed to default function
  const loginRes = http.post("https://api.example.com/auth/login", JSON.stringify({
    email: "loadtest@example.com",
    password: __ENV.TEST_PASSWORD,
  }), { headers: { "Content-Type": "application/json" } });

  const token = JSON.parse(loginRes.body).accessToken;
  return { token };
}

export default function (data) {
  const params = {
    headers: {
      Authorization: `Bearer ${data.token}`,
      "Content-Type": "application/json",
    },
  };
  const res = http.get("https://api.example.com/me", params);
  check(res, { "authenticated 200": (r) => r.status === 200 });
}

Custom metrics and tagging

import http from "k6/http";
import { Trend, Counter } from "k6/metrics";

const orderLatency = new Trend("order_latency");
const orderErrors = new Counter("order_errors");

export const options = {
  thresholds: {
    order_latency: ["p(99)<1000"],
    order_errors: ["count<10"],
  },
};

export default function () {
  const res = http.get("https://api.example.com/orders", {
    tags: { endpoint: "list_orders" }, // tag for filtering in dashboards
  });

  orderLatency.add(res.timings.duration);
  if (res.status !== 200) {
    orderErrors.add(1);
  }
}

Best Practices

  • Define thresholds for every test so CI pipelines can automatically pass or fail based on performance budgets; never rely on manually inspecting output after each run.
  • Use the setup() function for one-time operations like authentication or test data creation, and teardown() for cleanup; these run outside the VU loop and do not inflate metrics.
  • Start with a realistic baseline (the ramping-vus executor) before moving to stress or spike tests; establish what "normal" looks like so regressions are detectable.

Common Pitfalls

  • Writing k6 scripts that import Node.js modules (fs, path, axios); k6 uses its own JavaScript runtime (not Node.js), so only k6-specific modules and ES-compatible code work. Use k6/http instead of fetch/axios.
  • Setting unrealistically high VU counts on a single load generator machine without monitoring its own CPU and network; if the load generator is saturated, the test measures the generator's limits, not the target system's performance.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add testing-services-skills

Get CLI access →