Load Testing with k6
Write and run load tests with Grafana k6: install, scripting with stages and thresholds, running in CI, interpreting results, and avoiding common mistakes.
k6 is an open-source load testing tool from Grafana Labs. Tests are written in JavaScript but run on a fast Go runtime, so a single machine can generate substantial traffic. Use k6 to learn how your system behaves under expected and extreme load, to find the point where latency or errors degrade, and to catch performance regressions in CI before they reach users.
Quick start
# macOS
brew install k6
# Debian or Ubuntu: follow the Grafana apt repository instructions
# Docker
docker run --rm -i grafana/k6 run - < script.js
Run a script with k6 run script.js. Note that k6 uses its own JavaScript runtime, not Node.js, so npm packages are not available unless bundled.
Core concepts
- Virtual users (VUs): concurrent simulated users, each running your default function in a loop.
- Iteration: one execution of the default function.
- Stages: a ramp profile that changes the target VU count over time.
- Checks: assertions on responses that record pass or fail but do not stop the test.
- Thresholds: pass or fail criteria for the whole test, such as p95 latency under 500 ms. If a threshold fails, k6 exits with a non-zero code.
- Executors: scenario types, for example
ramping-vusfor closed models andconstant-arrival-ratefor open models.
A real script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 20 }, // ramp up
{ duration: '3m', target: 20 }, // steady state
{ duration: '30s', target: 0 }, // ramp down
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500', 'p(99)<1000'],
checks: ['rate>0.99'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://test.k6.io';
export default function () {
const res = http.get(`${BASE_URL}/`);
check(res, {
'status is 200': (r) => r.status === 200,
'body is not empty': (r) => r.body.length > 0,
});
const login = http.post(
`${BASE_URL}/login`,
JSON.stringify({ user: 'demo', pass: 'demo' }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(login, { 'login accepted': (r) => r.status < 400 });
sleep(1); // think time
}
Run it and pass configuration without editing the file:
k6 run -e BASE_URL=https://staging.example.com script.js
k6 run --vus 10 --duration 30s script.js
k6 run --out json=results.json script.js
Reading the results
The end-of-test summary lists http_req_duration with avg, min, med, max, p(90), and p(95), plus http_req_failed, iterations, and checks. Focus on percentiles rather than averages, and compare runs over time. For richer analysis, stream metrics to Prometheus via remote write or to Grafana Cloud k6 and view them next to your service dashboards.
Test types
- Smoke: one or two VUs for a minute, confirming the script and system work.
- Load: expected production traffic held for a sustained period.
- Stress: increasing load beyond normal to find the breaking point.
- Spike: a sudden surge, then recovery.
- Soak: moderate load for hours to expose leaks and degradation.
Running in CI
# .github/workflows/perf.yml (excerpt)
- name: Run k6 smoke test
uses: grafana/setup-k6-action@v1
- run: k6 run --vus 5 --duration 1m tests/smoke.js
Because thresholds set the exit code, a regression fails the pipeline automatically.
Common pitfalls
- Testing production-like data against a tiny environment. Results only mean something if the target resembles production.
- No think time. Without
sleep, each VU hammers as fast as possible, producing unrealistic load. - Closed versus open models. With VUs, slow responses reduce the request rate. Use arrival-rate executors to hold a fixed rate.
- Load generator bottlenecks. Watch the generator's CPU and network so you measure the system, not the client.
- Ignoring warm-up such as caches and connection pools, and hitting third-party services you do not own.
Where k6 fits in an architecture
k6 belongs in your delivery pipeline and your capacity planning. Run small smoke tests on every merge, larger load tests against staging before major releases, and periodic stress tests to validate autoscaling and rate limits. Pair test runs with monitoring so you can correlate user-facing latency with CPU, queue depth, and database metrics.
Key takeaways
- Write tests as code and keep them in version control.
- Use thresholds so tests pass or fail objectively.
- Model realistic user behaviour with think time and sensible data.
- Match the test type to the question: smoke, load, stress, spike, or soak.
- Watch both the target system and the load generator.