k6 Load Testing
Install and Setup
brew install k6 # macOS
sudo apt-get install k6 # Debian/Ubuntu (after adding k6 repo)
choco install k6 # Windows
docker run --rm -i grafana/k6 run - <script.js # Docker
k6 version # verify
Basic Test Script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = { vus: 10, duration: '30s' };
export default function () {
const getRes = http.get('https://test-api.k6.io/public/crocodiles/');
check(getRes, { 'GET status 200': (r) => r.status === 200 });
const payload = JSON.stringify({ name: 'test', sex: 'M', date_of_birth: '2020-01-01' });
const postRes = http.post('https://test-api.k6.io/anything', payload, {
headers: { 'Content-Type': 'application/json' },
});
check(postRes, { 'POST status 2xx': (r) => r.status >= 200 && r.status < 300 });
sleep(1);
}
Run with k6 run script.js.
Virtual Users and Duration
export const options = {
vus: 50, // 50 concurrent virtual users
duration: '5m', // run for 5 minutes
iterations: 1000, // or cap at 1000 total iterations across all VUs
};
Stages (Ramp Up, Steady State, Ramp Down)
export const options = {
stages: [
{ duration: '2m', target: 50 }, // ramp up
{ duration: '5m', target: 50 }, // steady state
{ duration: '2m', target: 0 }, // ramp down
],
};
Test Types
// Load test -- typical expected traffic
stages: [{ duration: '5m', target: 100 }, { duration: '10m', target: 100 }, { duration: '5m', target: 0 }]
// Stress test -- beyond normal capacity, stepped increases
stages: [
{ duration: '2m', target: 100 }, { duration: '5m', target: 100 },
{ duration: '2m', target: 200 }, { duration: '5m', target: 200 },
{ duration: '2m', target: 300 }, { duration: '5m', target: 300 },
{ duration: '5m', target: 0 },
]
// Soak test -- sustained load over hours
stages: [{ duration: '5m', target: 100 }, { duration: '8h', target: 100 }, { duration: '5m', target: 0 }]
// Spike test -- sudden surge
stages: [
{ duration: '1m', target: 10 }, { duration: '10s', target: 500 },
{ duration: '3m', target: 500 }, { duration: '10s', target: 10 }, { duration: '2m', target: 0 },
]
// Breakpoint test -- find system limits
{ executor: 'ramping-arrival-rate', startRate: 1, timeUnit: '1s', preAllocatedVUs: 500,
stages: [{ duration: '30m', target: 500 }] }
Checks (Assertions)
import { check } from 'k6';
check(res, {
'status is 200': (r) => r.status === 200,
'body contains expected text': (r) => r.body.includes('crocodile'),
'response time < 500ms': (r) => r.timings.duration < 500,
'content-type is JSON': (r) => r.headers['Content-Type'].includes('application/json'),
});
Thresholds (Pass/Fail Criteria)
export const options = {
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th percentile < 500ms
http_req_failed: ['rate<0.01'], // error rate < 1%
checks: ['rate>0.99'], // 99% of checks pass
http_reqs: ['rate>100'], // throughput > 100 req/s
'http_req_duration{name:login}': ['p(95)<300'], // threshold on tagged request
},
};
k6 exits non-zero when any threshold fails, suitable for CI gating.
HTTP Methods
import http from 'k6/http';
export default function () {
http.get('https://api.example.com/items');
http.post('https://api.example.com/items',
JSON.stringify({ name: 'widget' }), { headers: { 'Content-Type': 'application/json' } });
http.put('https://api.example.com/items/1',
JSON.stringify({ name: 'updated' }), { headers: { 'Content-Type': 'application/json' } });
http.del('https://api.example.com/items/1');
// Multipart file upload
const file = open('/path/to/file.png', 'b');
http.post('https://api.example.com/upload', {
file: http.file(file, 'file.png', 'image/png'),
});
}
Headers and Authentication
// Bearer token
http.get('https://api.example.com/protected', {
headers: { Authorization: 'Bearer eyJhbGciOi...', 'Content-Type': 'application/json' },
});
// Login in setup, pass token to default function
export function setup() {
const res = http.post('https://api.example.com/login',
JSON.stringify({ username: 'user', password: 'pass' }),
{ headers: { 'Content-Type': 'application/json' } });
return { token: res.json('token') };
}
export default function (data) {
http.get('https://api.example.com/dashboard', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
// Cookies
const jar = http.cookieJar();
jar.set('https://api.example.com', 'session_id', 'abc123');
Groups and Tags
import { group } from 'k6';
export default function () {
group('user flow', function () {
group('login', function () {
http.post('https://api.example.com/login',
JSON.stringify({ user: 'test', pass: 'test' }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } });
});
group('browse', function () {
http.get('https://api.example.com/items', { tags: { name: 'list-items' } });
});
});
}
Tags enable filtering in thresholds and outputs: http_req_duration{name:login}.
Parameterization
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
const users = new SharedArray('users', function () {
return JSON.parse(open('./users.json')); // [{username: "a", password: "b"}, ...]
});
const csvData = new SharedArray('csv', function () {
return papaparse.parse(open('./data.csv'), { header: true }).data;
});
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
http.post('https://api.example.com/login',
JSON.stringify({ username: user.username, password: user.password }),
{ headers: { 'Content-Type': 'application/json' } });
}
SharedArray loads data once and shares across VUs to reduce memory.
Custom Metrics
import { Counter, Gauge, Rate, Trend } from 'k6/metrics';
const errorCount = new Counter('custom_errors'); // cumulative count
const cacheHitRate = new Rate('cache_hits'); // percentage of true values
const pageLoadTime = new Trend('page_load_time'); // distribution (min/max/avg/p90/p95)
const activeConns = new Gauge('active_connections'); // last value
export default function () {
const res = http.get('https://api.example.com/');
pageLoadTime.add(res.timings.duration);
cacheHitRate.add(res.headers['X-Cache'] === 'HIT');
if (res.status !== 200) errorCount.add(1);
}
export const options = {
thresholds: { page_load_time: ['p(95)<600'], custom_errors: ['count<10'] },
};
Output
k6 run script.js # stdout summary
k6 run --out json=results.json script.js # JSON file
k6 run --out csv=results.csv script.js # CSV file
k6 run --out influxdb=http://localhost:8086/k6 script.js # InfluxDB
K6_CLOUD_TOKEN=token k6 run --out cloud script.js # Grafana Cloud
k6 run --out json=results.json --out influxdb=http://localhost:8086/k6 script.js # multiple
// Custom summary handler
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';
export function handleSummary(data) {
return {
stdout: textSummary(data, { indent: ' ', enableColors: true }),
'summary.json': JSON.stringify(data),
};
}
Browser Testing
import { browser } from 'k6/browser';
import { check } from 'k6';
export const options = {
scenarios: {
ui: { executor: 'shared-iterations', options: { browser: { type: 'chromium' } } },
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://test.k6.io/my_messages.php');
await page.locator('input[name="login"]').type('admin');
await page.locator('input[name="password"]').type('123');
await page.locator('input[type="submit"]').click();
const header = await page.locator('h2').textContent();
check(header, { 'logged in': (h) => h === 'Welcome, admin!' });
} finally { await page.close(); }
}
CI/CD Integration (GitHub Actions)
name: Load Test
on: [push, pull_request]
jobs:
k6-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: grafana/k6-action@v0.3.1
with:
filename: tests/load-test.js
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
Thresholds serve as the quality gate -- the step fails when any threshold is breached.
Common Patterns
API Endpoint Test
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [{ duration: '1m', target: 50 }, { duration: '3m', target: 50 }, { duration: '1m', target: 0 }],
thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.01'] },
};
export default function () {
check(http.get('https://api.example.com/health'), { 'status 200': (r) => r.status === 200 });
sleep(1);
}
Login Flow Test
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
const loginRes = http.post('https://api.example.com/auth/login',
JSON.stringify({ email: 'user@test.com', password: 'password123' }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } });
check(loginRes, { 'login succeeded': (r) => r.status === 200 });
const token = loginRes.json('access_token');
const profileRes = http.get('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` }, tags: { name: 'profile' } });
check(profileRes, { 'profile loaded': (r) => r.status === 200 });
sleep(1);
}
File Upload Test
import http from 'k6/http';
import { check } from 'k6';
const testFile = open('/path/to/test-file.pdf', 'b');
export default function () {
const res = http.post('https://api.example.com/upload', {
file: http.file(testFile, 'report.pdf', 'application/pdf'),
});
check(res, { 'upload ok': (r) => r.status === 200, 'under 5s': (r) => r.timings.duration < 5000 });
}