k6 is a modern, developer-centric load testing tool that helps you write and execute performance tests for HTTP APIs, WebSocket endpoints, and browser scenarios. This skill provides comprehensive guidance on writing realistic load tests, configuring test scenarios (smoke, load, stress, spike, soak), analyzing results, and integrating with CI/CD pipelines.
Use this skill when you need to validate system performance, identify bottlenecks, ensure SLA compliance, or catch performance regressions before deployment.
# macOS
brew install k6
# Windows
choco install k6
# Linux
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
// simple-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
const res = http.get('https://httpbin.test.k6.io/get');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Run with: k6 run simple-test.js
export const options = {
// Virtual Users (concurrent users)
vus: 100,
// Test duration
duration: '5m',
// Or use stages for ramp-up/ramp-down
stages: [
{ duration: '30s', target: 20 }, // Ramp up
{ duration: '1m', target: 100 }, // Stay at 100
{ duration: '30s', target: 0 }, // Ramp down
],
// Thresholds (SLA)
thresholds: {
http_req_duration: ['p(95)<500'], // 95% requests < 500ms
http_req_failed: ['rate<0.01'], // Error rate < 1%
},
// Load zones (distributed testing)
ext: {
loadimpact: {
name: 'My Load Test',
distribution: {
'amazon:us:ashburn': { weight: 50 },
'amazon:eu: Dublin': { weight: 50 },
},
},
},
};
| Type | Use Case | Configuration |
|---|---|---|
| Smoke Test | Verify basic functionality | Low VUs (1-5), short duration |
| Load Test | Normal expected load | Target VUs based on traffic |
| Stress Test | Find breaking point | Ramp beyond capacity |
| Spike Test | Sudden traffic spikes | Rapid increase/decrease |
| Soak Test | Long-term stability | Extended duration |
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
// GET request
const getRes = http.get('https://api.example.com/users');
check(getRes, {
'GET succeeded': (r) => r.status === 200,
'has users': (r) => r.json('data.length') > 0,
});
// POST request with JSON body
const postRes = http.post('https://api.example.com/users',
JSON.stringify({ name: 'Test User', email: 'test@example.com' }),
{
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + __ENV.API_TOKEN,
},
}
);
check(postRes, {
'POST succeeded': (r) => r.status === 201,
'user created': (r) => r.json('id') !== undefined,
});
sleep(1);
}
import http from 'k6/http';
import { check } from 'k6';
export default function () {
// Login and extract token
const loginRes = http.post('https://api.example.com/login',
JSON.stringify({ email: 'test@example.com', password: 'password123' })
);
const token = loginRes.json('access_token');
// Use token in subsequent requests
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
const profileRes = http.get('https://api.example.com/profile', {
headers: headers,
});
check(profileRes, {
'profile loaded': (r) => r.status === 200,
});
}
import http from 'k6/http';
import { check } from 'k6';
const usernames = ['user1', 'user2', 'user3', 'user4', 'user5'];
export default function () {
// Use shared array with VU-specific index
const username = usernames[__VU % usernames.length];
const res = http.get(`https://api.example.com/users/${username}`);
check(res, {
'user found': (r) => r.status === 200,
});
}
import { browser } from 'k6/browser';
export const options = {
scenarios: {
browser_test: {
executor: 'constant-vus',
vus: 5,
duration: '30s',
browser: {
type: 'chromium',
},
},
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://example.com');
const title = await page.title();
console.log(`Page title: ${title}`);
// Click and interact
await page.click('button[data-testid="submit"]');
// Wait for response
await page.waitForSelector('.success-message');
} finally {
await page.close();
}
}
Install browser support: k6 install chromium
import ws from 'k6/ws';
import { check } from 'k6';
export default function () {
const url = 'wss://echo.websocket.org';
ws.connect(url, {}, function (socket) {
socket.on('open', () => {
console.log('WebSocket connected');
socket.send('Hello WebSocket');
});
socket.on('message', (data) => {
console.log(`Received: ${data}`);
check(data, {
'echo received': (d) => d.includes('Hello'),
});
});
socket.on('close', () => {
console.log('WebSocket closed');
});
// Send periodic messages
socket.setInterval(function () {
socket.send('ping');
}, 1000);
// Close after 5 seconds
socket.setTimeout(function () {
socket.close();
}, 5000);
});
}
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
// Option 1: Load once, shared across VUs
const users = new SharedArray('users', function () {
return open('./users.csv').split('\n').slice(1).map(line => {
const [email, password] = line.split(',');
return { email, password };
});
});
export default function () {
const user = users[__VU % users.length];
const res = http.post('https://api.example.com/login',
JSON.stringify({ email: user.email, password: user.password })
);
check(res, { 'login successful': (r) => r.status === 200 });
}
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
const products = new SharedArray('products', function () {
return JSON.parse(open('./products.json'));
});
export default function () {
const product = products[Math.floor(Math.random() * products.length)];
const res = http.get(`https://api.example.com/products/${product.id}`);
check(res, { 'product found': (r) => r.status === 200 });
}
export const options = {
vus: 50,
duration: '2m',
thresholds: {
// Response time thresholds
http_req_duration: ['p(95)<500', 'p(99)<1000'],
// Error rate threshold
http_req_failed: ['rate<0.01'],
// Throughput threshold
http_reqs: ['rate>100'],
},
};
export const options = {
thresholds: {
// Multiple thresholds on same metric
http_req_duration: [
'p(90)<300', // 90th percentile < 300ms
'p(95)<500', // 95th percentile < 500ms
'p(99)<1000', // 99th percentile < 1s
'avg<200', // average < 200ms
],
// Custom metrics
my_custom_metric: ['avg<100'],
// Abort on threshold failure
'http_req_duration{method:GET}': ['p(95)<300'],
},
};
import http from 'k6/http';
import { Counter, Trend, Rate, Gauge } from 'k6/metrics';
// Define custom metrics
const myCounter = new Counter('api_calls_total');
const responseTime = new Trend('response_time');
const errorRate = new Rate('error_rate');
const activeUsers = new Gauge('active_users');
export default function () {
const res = http.get('https://api.example.com/data');
// Increment counter
myCounter.add(1);
// Add to trend (for percentiles)
responseTime.add(res.timings.duration);
// Track error rate
errorRate.add(res.status !== 200);
// Set gauge value
activeUsers.add(__VU);
// Tagged metrics
const taggedRes = http.get('https://api.example.com/users', {
tags: { endpoint: 'users', env: 'prod' },
});
}
# .github/workflows/load-test.yml
name: Load Tests
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup k6
uses: grafana/k6-action@v0.2.0
- name: Run load test
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
run: k6 run --out json=results.json load-test.js
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: k6-results
path: results.json
- name: Check thresholds
if: failure()
run: |
echo "Load test failed thresholds!"
exit 1
# .gitlab-ci.yml
load_test:
image: grafana/k6:latest
script:
- k6 run load-test.js
artifacts:
when: always
paths:
- results.json
reports:
junit: results.xml
# Text summary
k6 run load-test.js
# JSON output for parsing
k6 run --out json=results.json load-test.js
# InfluxDB + Grafana
k6 run --out influxdb=http://localhost:8086/k6 load-test.js
# Prometheus remote write
k6 run --out prometheus=localhost:9090/k6 load-test.js
# Cloud results
k6 run --out cloud load-test.js
| Metric | Description | Good | Warning | Bad |
|---|---|---|---|---|
| http_req_duration (p95) | 95% response time | < 300ms | 300-500ms | > 500ms |
| http_req_failed | Error rate | < 0.1% | 0.1-1% | > 1% |
| http_reqs | Requests/sec | Meeting target | Near limit | At limit |
| vus | Virtual users | Stable | Gradual increase | Unexpected spike |
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50,
duration: '2m',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
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,
});
sleep(1);
}
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', function () {
return JSON.parse(open('./users.json'));
});
export default function () {
const user = users[__VU % users.length];
const loginRes = http.post('https://api.example.com/login',
JSON.stringify({ email: user.email, password: user.password })
);
const token = loginRes.json('access_token');
const headers = { 'Authorization': `Bearer ${token}` };
const res = http.get('https://api.example.com/profile', { headers });
check(res, { 'profile loaded': (r) => r.status === 200 });
}
tags: { endpoint: 'users' })Problem: Tests pass locally but fail in CI Solution: Ensure CI environment has similar resources and network conditions
Problem: Inconsistent results between runs Solution: Check for external dependencies, random data, or test data pollution
Problem: k6 runs out of memory
Solution: Use SharedArray for large data, reduce VUs, or use --max-memory flag
Problem: Thresholds too strict Solution: Start with relaxed thresholds, tighten based on historical data
@performance-engineer - For broader performance optimization@api-testing-observability-api-mock - For API mocking during testing@application-performance-performance-optimization - For performance optimization