Production engineering guide for load testing k6 covering patterns, implementation strategies, and operational best practices.
Load testing with k6 is a way to simulate real-world user traffic on a system, measuring performance under sustained or bursty load. It matters when you need to predict how an API, service, or web application behaves under pressure—before release, after a deployment, or to validate a new architecture. k6 is the engine: a modern, open-source load testing tool built for developers, with a scripting language that blends JavaScript with performance instrumentation.
A k6 test begins with a script written in JavaScript, executed by the k6 binary. The script defines how users interact with the system over time. The core of every test is the export default function that serves as the entry point for virtual users.
import http from 'k6/http';
import { check, sleep } from 'k6';
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);
}
This script runs once per virtual user (VU), and is typically executed in a loop over a defined duration. The sleep function introduces a think time between requests, simulating real user behavior.
To run this test, use the k6 run command:
k6 run script.js
By default, k6 runs one VU for 10 seconds. To customize the test, pass flags:
k6 run -u 100 -d 30s script.js
This starts 100 virtual users, each running the script for 30 seconds. The -u flag sets the number of VUs, and -d sets the duration.
k6 supports a declarative configuration via a k6.json file at the project root. This file is used to define test settings that are not hard-coded in the script.
{
"duration": "60s",
"vus": 50,
"output": ["csv", "json"],
"thresholds": {
"http_req_duration": ["avg<200", "p(95)<500"],
"http_req_failed": ["rate<0.01"]
},
"globals": {
"API_BASE_URL": "https://api.example.com"
}
}
The duration and vus keys control the overall test shape. The output array specifies where k6 writes results—csv and json are built-in formats, writing to k6-results.csv and k6-results.json by default.
Thresholds define performance goals. A threshold is a condition on a metric that must be met across the test. The http_req_duration threshold checks that the average request duration is under 200ms, and the 95th percentile under 500ms. If any threshold fails, k6 exits with code 1.
The globals section exposes values to the test script via __ENV:
export default function () {
const baseUrl = __ENV.API_BASE_URL;
const res = http.get(`${baseUrl}/users`);
// ...
}
To run tests with different environments or configurations, use the --env flag:
k6 run --env staging script.js
k6 looks for a k6-environment-staging.json file. If it exists, k6 merges it into the base k6.json, overriding values.
For example, k6-environment-production.json might contain:
{
"vus": 200,
"duration": "5m",
"thresholds": {
"http_req_duration": ["avg<300", "p(99)<1000"]
}
}
This allows reusing the same script across environments without duplicating code.
k6 supports ramping load profiles through the --rps and --constant-vus flags, but the most powerful way is to define a scenario in the script.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
login: {
executor: 'ramping-vus',
startVUs: 10,
stages: [
{ duration: '10s', target: 50 },
{ duration: '30s', target: 100 },
{ duration: '20s', target: 50 },
{ duration: '10s', target: 0 },
],
},
api_browsing: {
executor: 'constant-vus',
vus: 20,
duration: '2m',
},
},
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
The options object defines multiple scenarios. The ramping-vus executor gradually increases the number of VUs across stages, ideal for simulating traffic spikes. constant-vus keeps a fixed number of VUs running for the entire duration.
When running such a script, k6 executes all scenarios in parallel:
k6 run script.js
Each scenario runs independently, but shares the same test script and global state.
k6 automatically collects metrics during execution. The primary metrics include:
http_req_duration: time from request start to response completionhttp_req_size: size of the response bodyhttp_reqs: total number of HTTP requestshttp_req_failed: count of failed requestsThese can be used in thresholds, and are also available in the output files.
For custom metrics, use check and metrics:
import { Counter, Gauge, Rate } from 'k6/metrics';
const apiErrors = new Counter('api_errors');
const userSessionTime = new Gauge('user_session_time');
export default function () {
const res = http.get('https://api.example.com/users');
if (res.status !== 200) {
apiErrors.add(1);
}
userSessionTime.set(res.timings.duration);
sleep(2);
}
The Counter and Gauge types are part of k6’s built-in metrics system. Rate is a metric that tracks the fraction of successful operations over time.
k6 integrates smoothly into CI systems. The exit code from k6 is critical: a non-zero exit code means at least one threshold failed.
In a CI pipeline, this allows automated performance gates:
- name: Run k6 load test
run: |
k6 run --env staging script.js
if [ $? -ne 0 ]; then
echo "🔴 Load test failed"
exit 1
fi
k6 can also output to external systems. The --out flag enables output to multiple destinations:
k6 run --out csv=results.csv --out json=results.json --out influxdb=http://influx.example.com:8086 script.js
The influxdb output driver writes metrics directly to an InfluxDB instance, enabling long-term monitoring and dashboarding.
k6 produces structured logs and artifacts. By default, k6 logs to stdout and writes a k6-results.json file. The JSON output contains all metrics, thresholds, and per-request details.
When a test fails, the k6-results.json file includes a thresholds section with the result of each check:
{
"thresholds": [
{
"name": "http_req_duration",
"actual": 250,
"target": "avg<200",
"passed": false
},
{
"name": "http_req_failed",
"actual": 0.012,
"target": "rate<0.01",
"passed": false
}
]
}
This makes it easy to parse results in CI or alerting systems.
To debug a failing test, use console.log in the script:
console.log('User ID:', userId);
console.log('Response body:', res.body);
k6 outputs logs at the start of each VU, and logs are written to k6-logs.json when the --log-format json flag is used.
k6 scripts can maintain state across VUs. Each VU has its own memory space, but state can be shared through global variables or external storage.
For example, simulating a user session:
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
const userId = __VU;
const token = `Bearer user-${userId}`;
const res = http.get('https://api.example.com/profile', {
headers: { Authorization: token },
});
check(res, {
'profile loaded': (r) => r.status === 200,
});
sleep(3);
}
Each VU has a unique __VU value, starting from 1. This allows simulating distributed user sessions.
To simulate login state, use __ENV and __VU together:
const baseUrl = __ENV.API_BASE_URL;
const userId = __VU;
const loginRes = http.post(`${baseUrl}/login`, {
username: `user-${userId}`,
password: 'secret',
});
const token = loginRes.json('token');
const headers = { Authorization: `Bearer ${token}` };
const res = http.get(`${baseUrl}/dashboard`, { headers });
This script logs in each VU independently, creating realistic user behavior.
One common failure is misinterpreting the duration and stages in ramping-vus. The stages array defines the target number of VUs over time, but k6 does not wait for the full duration of each stage unless explicitly configured. If the duration in options is not aligned with the total time of all stages, k6 may finish early or continue beyond the last stage.
Another issue is the sleep function: it runs on the VU thread but does not block the entire test. A sleep(1) in a VU does not pause all VUs; each VU sleeps independently.
The check function returns a boolean, but it does not stop the test on failure. To make checks fail the test, use check(res, {...}) and then if (!check(...)) { exit(1); }.
k6 does not automatically retry failed requests. To implement retry logic, use a for loop with http.get and check inside:
for (let i = 0; i < 3; i++) {
const res = http.get('https://api.example.com/users');
if (res.status === 200) {
break;
}
sleep(1);
}
Finally, k6’s default output format is not always sufficient. The k6-results.json file can grow large, especially under high load. To reduce file size, use --out json=results.json with --json-compress:
k6 run --out json=results.json --json-compress script.js
This enables gzip compression on the output, reducing disk and network overhead.
k6 is a developer-first tool for load testing. It combines a simple, expressive scripting language with powerful execution modes, thresholds, and output formats. Its configuration is declarative and composable, and it integrates seamlessly into any CI/CD pipeline. The most common mistake is assuming that duration and stages align perfectly without verifying the total time. The most valuable insight is that k6’s exit code is the primary signal for test success—use it to gate deployments, and validate thresholds in every run.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy