k6
k6 is a code-first load testing tool written in Go, scripted in JavaScript/TypeScript — VU model, threshold-based pass/fail, native WebSocket support, and first-class CI integration via exit codes.
k6 is an open-source load testing tool built by Grafana Labs. Tests are written in JavaScript (TypeScript supported via transpilation), run by a Go runtime, and produce threshold-based pass/fail outcomes suitable for CI integration. The Go VU model is significantly lighter than JMeter's JVM-thread-per-VU model, allowing higher concurrency on the same hardware.
See technical-qa/jmeter for the comparison with Apache JMeter and test-automation/performance-testing for the broader performance test taxonomy.
Core Concepts
Virtual Users (VUs) — concurrent simulated users. Each VU runs the default function in a loop for the duration of the test. k6 uses Go goroutines per VU; a single machine can sustain thousands of VUs without the heap pressure that JMeter's thread-per-VU model creates.
Thresholds — pass/fail criteria expressed in the test script. CI exits with code 0 when all thresholds pass, non-zero when any fail. This makes k6 natively usable as a CI gate without a separate threshold-checking script.
Checks — assertions within test code. A check failure increments a counter but does not abort the test. Checks feed into the checks rate metric, which can be used in a threshold.
Basic Script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50,
duration: '5m',
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 must pass
},
};
export default function () {
const res = http.get('https://api.example.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
'has products': (r) => JSON.parse(r.body).length > 0,
});
sleep(1);
}Ramp Profiles with Stages
export const options = {
stages: [
{ duration: '2m', target: 10 }, // ramp up to 10 VUs
{ duration: '5m', target: 100 }, // ramp to 100 VUs
{ duration: '10m', target: 100 }, // hold 100 VUs (steady state)
{ duration: '2m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<1000', 'p(99)<2000'],
},
};POST Requests with JSON Body
import http from 'k6/http';
import { check } from 'k6';
export default function () {
const payload = JSON.stringify({ productId: 'abc123', quantity: 1 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post('https://api.example.com/orders', payload, params);
check(res, {
'order created': (r) => r.status === 201,
'order id present': (r) => JSON.parse(r.body).orderId !== undefined,
});
}WebSocket Support
k6 supports WebSocket testing via k6/experimental/websockets (stable in k6 v0.43+):
import { WebSocket } from 'k6/experimental/websockets';
import { check } from 'k6';
export default function () {
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', channel: 'orders' }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
check(msg, { 'received order event': (m) => m.type === 'order_created' });
ws.close();
};
}For sustained WebSocket load tests, use k6/ws (the older API) with a controlled session.Duration so sessions close after a defined period rather than running until the test ends.
CI Integration (GitHub Actions)
- name: Run load test
run: k6 run --vus 20 --duration 60s tests/load/api.js
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: k6-results
path: results.jsonk6 exits with code 1 if any threshold is breached. GitHub Actions interprets this as a failed step, blocking the pipeline without additional tooling.
k6 Cloud
Grafana's hosted execution environment for k6. Provides: geographically distributed load generation, real-time dashboards, historical test comparison, and team collaboration. Use when peak VU count exceeds what a single CI runner can generate (~500-1000 VUs depending on think time and response size).
k6 cloud tests/load/api.jsRequires a K6_CLOUD_TOKEN environment variable and projectID set in the script options.
k6 vs JMeter
| Dimension | k6 | JMeter |
|---|---|---|
| Test definition | JavaScript/TypeScript | XML + GUI |
| VU model | Go goroutine (lightweight) | JVM thread (heavier) |
| CI integration | Native (threshold exit codes) | Requires wrapper script |
| WebSocket | Native (experimental/websockets) | Plugin required |
| Non-HTTP protocols | HTTP, WS, gRPC via extensions | JDBC, JMS, LDAP, FTP, TCP natively |
| Existing legacy suites | No .jmx compat | Full .jmx ecosystem |
Choose k6 for greenfield, developer-led performance testing. Choose JMeter when the client has existing .jmx files or needs non-HTTP protocol coverage. See technical-qa/jmeter for the full decision framework.
Connections
- technical-qa/jmeter — Apache JMeter; comparison and migration path
- technical-qa/websocket-testing — WebSocket-specific test patterns covering k6 and Playwright
- technical-qa/performance-testing — performance test taxonomy: load, stress, soak, spike tests
Open Questions
- What's the practical VU count ceiling on a single k6 runner before needing k6 Cloud or distributed k6 instances?
- How do you model realistic user think time in k6 without introducing so much sleep that the test runs for hours?
- When does k6 + Grafana Cloud become more cost-effective than running k6 locally and storing metrics in InfluxDB?
Related reading