TL;DR:
- k6 is a load testing tool where tests are JavaScript scripts — it uses Go under the hood for performance but your test logic is plain JS, so there’s no XML config or proprietary DSL to learn
- A basic k6 test is 5–10 lines; a realistic production API test is 50–100 lines covering authentication, scenario weights, and SLA thresholds
- k6 integrates directly with Grafana for real-time dashboard output, and the Grafana k6 Cloud offering (or self-hosted Grafana setup) lets you store and compare results across test runs
Why Load Test at All
Most teams discover performance problems in production, where discovery is expensive. Load testing shifts that discovery left — you find out your API can’t handle 500 concurrent users during a scheduled test rather than during a product launch.
k6 hits a useful design point: it’s more capable than most “just ping the URL” tools but less ceremonial than enterprise testing platforms. Tests are code, so they live in version control alongside your application. The output is structured and scriptable, so CI pipelines can fail on performance regressions just as they fail on broken unit tests.
Installation
# macOS
brew install k6
# Windows
choco install k6
# Linux (Debian/Ubuntu)
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \
| sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
# Docker
docker run --rm -i grafana/k6 run - <script.js
Your First Test
// simple-test.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
vus: 10, // 10 virtual users
duration: "30s", // run for 30 seconds
};
export default function () {
const response = http.get("https://api.example.com/products");
check(response, {
"status is 200": (r) => r.status === 200,
"response time < 500ms": (r) => r.timings.duration < 500,
});
sleep(1); // wait 1 second between iterations (simulates think time)
}
k6 run simple-test.js
The output shows requests per second, median response time, 95th and 99th percentile latencies, error rates, and check pass rates. 10 VUs with a 1-second sleep means roughly 10 requests per second across the 30-second run — 300 total requests.
Realistic API Tests with Authentication
Most APIs require authentication. k6 handles this in the setup function, which runs once before the main test and can return data to the test function:
// api-test.js
import http from "k6/http";
import { check, sleep } from "k6";
import { SharedArray } from "k6/data";
const BASE_URL = __ENV.BASE_URL || "https://api.example.com";
// SharedArray: loaded once, shared across all VUs (memory-efficient)
const users = new SharedArray("users", function () {
return JSON.parse(open("./test-users.json"));
});
export const options = {
thresholds: {
http_req_duration: ["p(95)<500"], // 95th percentile under 500ms
http_req_failed: ["rate<0.01"], // error rate under 1%
checks: ["rate>0.99"], // >99% of checks pass
},
scenarios: {
normal_load: {
executor: "ramping-vus",
startVUs: 0,
stages: [
{ duration: "2m", target: 50 }, // ramp up to 50 VUs
{ duration: "5m", target: 50 }, // hold at 50 VUs
{ duration: "2m", target: 0 }, // ramp down
],
},
},
};
export function setup() {
// Authenticate and return token for use in tests
const user = users[0];
const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: user.email,
password: user.password,
}), { headers: { "Content-Type": "application/json" } });
check(loginRes, { "login successful": (r) => r.status === 200 });
return { token: loginRes.json("access_token") };
}
export default function (data) {
const params = {
headers: {
"Authorization": `Bearer ${data.token}`,
"Content-Type": "application/json",
},
};
// Browse products
const productsRes = http.get(`${BASE_URL}/products?page=1&limit=20`, params);
check(productsRes, {
"products status 200": (r) => r.status === 200,
"products returns items": (r) => r.json("items").length > 0,
});
sleep(Math.random() * 2 + 0.5); // random think time 0.5–2.5 seconds
// Add to cart
const cartRes = http.post(`${BASE_URL}/cart`, JSON.stringify({
product_id: "prod_123",
quantity: 1,
}), params);
check(cartRes, { "add to cart success": (r) => r.status === 201 });
sleep(1);
}
Thresholds: Making Tests Pass or Fail
Without thresholds, k6 runs and reports — but CI doesn’t know if the performance was acceptable. Thresholds turn k6 into a quality gate:
export const options = {
thresholds: {
// All HTTP requests complete in under 500ms (95th percentile)
http_req_duration: ["p(95)<500"],
// Under 1% error rate
http_req_failed: ["rate<0.01"],
// Specific endpoint: checkout must be under 1 second
"http_req_duration{endpoint:checkout}": ["p(99)<1000"],
// Custom metric you define and track
order_creation_duration: ["avg<300", "p(95)<600"],
},
};
When any threshold is breached, k6 exits with a non-zero status code, which fails the CI step.
Custom Metrics
Track business-level metrics beyond the built-in HTTP stats:
import { Trend, Rate } from "k6/metrics";
const orderDuration = new Trend("order_creation_duration", true); // in milliseconds
const orderFailRate = new Rate("order_fail_rate");
export default function (data) {
const start = Date.now();
const res = http.post(`${BASE_URL}/orders`, orderPayload, params);
orderDuration.add(Date.now() - start);
orderFailRate.add(res.status !== 201);
check(res, { "order created": (r) => r.status === 201 });
}
Scenarios for Realistic Traffic Patterns
Real traffic isn’t 50 VUs all doing the same thing. Scenarios let you model mixed workloads:
export const options = {
scenarios: {
// Browsing users: many, low activity
browsers: {
executor: "constant-vus",
vus: 100,
duration: "10m",
exec: "browseProducts",
},
// Purchasing users: fewer, higher activity
purchasers: {
executor: "constant-arrival-rate",
rate: 5, // 5 purchases per second
timeUnit: "1s",
duration: "10m",
preAllocatedVUs: 20,
maxVUs: 50,
exec: "purchase",
},
},
};
export function browseProducts() { /* ... */ }
export function purchase() { /* ... */ }
CI/CD Integration
# GitHub Actions: run k6 test on every PR
name: Load Test
on:
pull_request:
paths:
- "api/**"
- "k6/**"
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6 load test
uses: grafana/k6-action@v0.3.1
with:
filename: k6/api-test.js
env:
BASE_URL: ${{ secrets.STAGING_API_URL }}
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }} # optional: send results to Grafana Cloud
For local development, run against a local server:
# Run against local dev server with environment variable
BASE_URL=http://localhost:3000 k6 run api-test.js
# Output JSON results for post-processing
k6 run --out json=results.json api-test.js
# Stream to a local InfluxDB instance for Grafana dashboard
k6 run --out influxdb=http://localhost:8086/k6 api-test.js
Grafana Output for Real-Time Dashboards
k6’s --out influxdb flag streams metrics in real time to an InfluxDB instance, which Grafana can display as a live dashboard during the test run. Grafana Labs provides an official k6 dashboard template (Dashboard ID: 2587) that shows VUs, RPS, response times, and error rates in real time.
For teams already running Grafana + InfluxDB for application monitoring, this lets you overlay load test metrics with application-level metrics (database query times, cache hit rates, queue depth) during the same time window — the most useful view for diagnosing what breaks under load.
When to Use k6 vs Other Tools
- k6 vs Artillery: Both are JavaScript-based load testing tools. k6 generally has better performance at high VU counts and tighter Grafana integration. Artillery has simpler YAML-based test definitions for straightforward cases.
- k6 vs Locust: Locust uses Python (better if your team knows Python better than JS), has a web UI for interactive test control, but runs slower per VU than k6 for the same server resources.
- k6 vs JMeter: JMeter is more feature-complete for complex protocol testing (JDBC, JMS, etc.) but has a steep XML-based configuration learning curve. For REST API and HTTP load testing, k6 is faster to set up and maintain.