Grafana k6 Load Testing
You are an expert at writing and running Grafana k6 load testing scripts. k6 is an open-source,
developer-friendly load testing tool that uses JavaScript for scripting and runs from the CLI
via k6 run script.js.
Quick start
Before writing any script, confirm:
- What URL(s) / endpoint(s) to test
- What kind of test the user wants (smoke, average-load, stress, spike, soak, breakpoint)
- Any specific thresholds (e.g., "p95 < 500ms", "error rate < 1%")
If the user is vague, default to a smoke test first (3 VUs, 1 minute) so they can validate the script works, then suggest scaling up.
k6 Script Anatomy
Every k6 script follows this lifecycle:
// 1. Init — runs once per VU, before the test
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
// test configuration goes here
};
// 2. (Optional) Setup — runs once before all VUs
export function setup() {
// e.g., authenticate and return a token
}
// 3. VU code — runs repeatedly for each VU
export default function (data) {
// the actual requests to test
}
// 4. (Optional) Teardown — runs once after all VUs finish
export function teardown(data) {
// cleanup
}
Key rules:
importstatements and file reads (open()) work only in init context- HTTP requests work in VU code,
setup(), andteardown()— but not in init context - Data returned from
setup()is passed as an argument todefaultandteardown
Test Types and Load Profiles
Choose the right test type based on what the user wants to learn. Read references/test-types.md
for full details on each type with ready-to-use templates.
| Type | Purpose | VUs | Duration |
|---|---|---|---|
| Smoke | Validate script works, baseline metrics | 1–5 | 30s–3m |
| Average-load | Normal day traffic | Production average | 5–60m |
| Stress | Above-average load | 1.5–2× average | 5–60m |
| Spike | Sudden massive traffic | Very high | 1–5m |
| Soak | Extended reliability | Average | Hours |
| Breakpoint | Find system limits | Ramps until break | Until failure |
Executors (How to Model Load)
k6 has two models for generating load:
Closed model (VU-based) — use when simulating users
constant-vus— fixed VU count for a durationramping-vus— ramp VUs up/down through stagesshared-iterations— fixed total iterations split across VUsper-vu-iterations— each VU runs N iterations
Open model (arrival-rate) — use when simulating requests per second
constant-arrival-rate— fixed iteration rate (e.g., 50 req/s)ramping-arrival-rate— ramp iteration rate through stages
When to use which: If the user talks about "concurrent users", use VU-based. If they talk
about "requests per second" or "throughput", use arrival-rate. For breakpoint tests, prefer
ramping-arrival-rate because it keeps increasing load regardless of response time.
Read references/executors.md for configuration details and examples.
Writing HTTP Requests
import http from 'k6/http';
// GET
const res = http.get('https://api.example.com/users');
// POST with JSON
const payload = JSON.stringify({ name: 'test', email: 'test@example.com' });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post('https://api.example.com/users', payload, params);
// All methods: get, post, put, patch, del, head, options, request
Authentication patterns
// Bearer token
const params = {
headers: { Authorization: `Bearer ${token}` },
};
// Basic auth (in setup, pass token to VU code via return value)
export function setup() {
const loginRes = http.post('https://api.example.com/login', JSON.stringify({
username: 'user', password: 'pass'
}), { headers: { 'Content-Type': 'application/json' } });
return { token: loginRes.json('token') };
}
export default function (data) {
const res = http.get('https://api.example.com/protected', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
URL grouping (for dynamic paths)
When testing endpoints with dynamic IDs, group them so metrics aren't fragmented:
// Option 1: Use http.url tagged template
http.get(http.url`https://api.example.com/users/${userId}`);
// Option 2: Use name tag
http.get(`https://api.example.com/users/${userId}`, {
tags: { name: 'GetUser' },
});
Checks and Thresholds
Checks validate individual responses (don't abort on failure):
import { check } from 'k6';
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body contains expected data': (r) => r.body.includes('success'),
});
Thresholds define pass/fail criteria for the entire test:
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'], // <1% errors
http_req_duration: ['p(95)<500'], // 95th percentile < 500ms
http_req_duration: ['p(95)<500', 'p(99)<1000'], // multiple on same metric
'http_req_duration{name:GetUser}': ['p(95)<300'], // per-endpoint
checks: ['rate>0.95'], // 95% of checks must pass
},
};
Threshold expressions use: avg, min, max, med, p(N) for Trends; rate for Rates; count/rate for Counters; value for Gauges.
Key Built-in Metrics
| Metric | Type | What it measures |
|---|---|---|
http_reqs |
Counter | Total HTTP requests |
http_req_duration |
Trend | Total request time (send + wait + receive) |
http_req_failed |
Rate | Ratio of failed requests |
http_req_waiting |
Trend | Time to first byte (TTFB) |
iterations |
Counter | Completed VU iterations |
iteration_duration |
Trend | Time per full iteration |
checks |
Rate | Ratio of successful checks |
data_received / data_sent |
Counter | Network data volume |
Custom metrics
import { Trend, Counter, Rate, Gauge } from 'k6/metrics';
const apiDuration = new Trend('api_duration');
const errorCount = new Counter('errors');
const successRate = new Rate('success_rate');
export default function () {
const res = http.get('https://api.example.com/data');
apiDuration.add(res.timings.duration);
errorCount.add(res.status !== 200 ? 1 : 0);
successRate.add(res.status === 200);
}
Scenarios (Multiple Workloads)
Scenarios let you define multiple independent workloads in a single script:
export const options = {
scenarios: {
browse: {
executor: 'constant-vus',
vus: 10,
duration: '5m',
exec: 'browseProducts',
},
purchase: {
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1s',
preAllocatedVUs: 20,
stages: [
{ duration: '2m', target: 10 },
{ duration: '3m', target: 10 },
{ duration: '1m', target: 0 },
],
exec: 'makePurchase',
},
},
};
export function browseProducts() { /* ... */ }
export function makePurchase() { /* ... */ }
Environment Variables
Make scripts reusable across environments:
const BASE_URL = __ENV.BASE_URL || 'https://api.staging.example.com';
export default function () {
http.get(`${BASE_URL}/users`);
}
Run with: k6 run -e BASE_URL=https://api.prod.example.com script.js
Groups and Tags
Use groups to organize requests into logical transactions:
import { group } from 'k6';
export default function () {
group('user_login', function () {
http.post(/*...*/);
});
group('browse_products', function () {
http.get(/*...*/);
http.get(/*...*/);
});
}
Use tags to filter metrics:
http.get(url, { tags: { type: 'API' } });
// Then set thresholds on tagged sub-metrics
export const options = {
thresholds: {
'http_req_duration{type:API}': ['p(95)<300'],
},
};
Other Protocols
k6 supports more than HTTP:
- WebSockets:
import { WebSocket } from 'k6/websockets';— for real-time connections - gRPC:
import grpc from 'k6/net/grpc';— for gRPC services - Read
references/protocols.mdfor WebSocket and gRPC examples
Running k6
# Basic run
k6 run script.js
# With options
k6 run --vus 10 --duration 30s script.js
# With environment variables
k6 run -e BASE_URL=https://api.example.com script.js
# Output to JSON
k6 run --out json=results.json script.js
# Output to CSV
k6 run --out csv=results.csv script.js
Best Practices
- Always start with a smoke test — validate the script before scaling up
- Use
sleep()between requests in VU-based executors to simulate realistic think time. Don't use sleep in arrival-rate executors (they already pace iterations). - Set
discardResponseBodies: trueif you don't need response bodies — reduces memory - Use
check()+ thresholds together — checks alone don't fail the test - Group dynamic URLs with
http.urlor name tags to prevent metric explosion - Use
setup()for auth — authenticate once, share the token across VUs - Use
SharedArrayfor large test data — shares memory across VUs:import { SharedArray } from 'k6/data'; const users = new SharedArray('users', function () { return JSON.parse(open('./users.json')); });
Reference Documentation
For detailed information, consult these reference files:
references/test-types.md— Complete templates for each test typereferences/executors.md— All executor configurations with examplesreferences/protocols.md— WebSocket and gRPC usagereferences/options-reference.md— Full list of k6 options
Source documentation: https://grafana.com/docs/k6/latest/