The most surprising thing about k6 is how little it feels like a load testing tool; it feels like writing JavaScript.
Let’s see it in action. Imagine you have a simple API running locally on port 3000, exposed at /health. We want to hit it 100 times, with a 1-second delay between each iteration.
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 1,
iterations: 100,
duration: '60s', // This is a safety net, iterations will cap us first.
};
export default function () {
http.get('http://localhost:3000/health');
sleep(1);
}
Save this as test.js. To run it, you’ll need k6 installed. On macOS, brew install k6. On Linux, download the binary from the k6 releases page. Once installed, navigate to the directory where you saved test.js and run:
k6 run test.js
You’ll see output like this:
/\ | cyclohexane
/ \ |
/ .. \ |
/ | | \ |
/ |__| \ |
/ / \ \ |
/ / \ \|
/ /______\ \
/_____________\ |
loading... executor 'default', duration: 60s, gracefulStop: 30s
#0 INFO[0000] Get http://localhost:3000/health - 200 OK source=http_client
#1 INFO[0001] Get http://localhost:3000/health - 200 OK source=http_client
#2 INFO[0002] Get http://localhost:3000/health - 200 OK source=http_client
...
#99 INFO[0099] Get http://localhost:3000/health - 200 OK source=http_client
executor 'default': iterations=100, vus=1, duration=60s
✓ script: test.js
✓ http_req_failed.....: 0.00% ✓ ✓ ✓ ✓ ✓
✓ http_req_duration...: avg=250ms min=200ms med=250ms max=300ms p(95)=300ms
✓ http_req_sending....: avg=0.02ms min=0.01ms med=0.02ms max=0.03ms p(95)=0.03ms
✓ http_req_receiving..: avg=0.05ms min=0.04ms med=0.05ms max=0.06ms p(95)=0.06ms
✓ http_req_tls........: avg=N/A min=N/A med=N/A max=N/A p(95)=N/A
✓ http_req_connect....: avg=100ms min=90ms med=100ms max=110ms p(95)=110ms
✓ http_req_waiting....: avg=249.9ms min=199.9ms med=249.9ms max=299.9ms p(95)=299.9ms
✓ http_req_blocked....: avg=0ms min=0ms med=0ms max=0ms p(95)=0ms
✓ http_req_dur......: avg=250ms min=200ms med=250ms max=300ms p(95)=300ms
✓ checks................: 100.00% ✓ ✓ ✓ ✓ ✓
✓ http_req_failed.....: 0.00% ✓ ✓ ✓ ✓ ✓
✓ data_sent...........: 7.5 kB 1.19 kB/s
✓ data_received.......: 22.2 kB 3.53 kB/s
✓ iteration_rate......: 1.0178/s
iterations...: 100 0.00% <-- 100 iterations completed
vus..........: 1 0.00%
time (avg)..: 98.34s 0.00%
time (max)...: 98.34s 0.00%
http_req_failed: 0 0.00%
INFO[0099] Try to run `k6 run --help` for more options.
The options object controls your test execution. vus (virtual users) are concurrent users, and iterations is the total number of requests. The default function is what each VUser executes. http.get makes the request, and sleep(1) pauses for one second, creating that 1-second gap between iterations.
The output shows the raw logs for each request, followed by a summary. http_req_duration is your key metric here, showing the total time for each request, broken down into stages like http_req_connect (TCP handshake) and http_req_waiting (time to first byte). The checks metric confirms that all requests were successful (100%).
The problem k6 solves is the difficulty of simulating realistic user load on an application. Instead of manually opening hundreds of browser tabs or writing complex shell scripts with curl, you write concise JavaScript that defines user behavior. This allows you to test performance under stress, identify bottlenecks, and ensure your API can handle the expected traffic.
The k6/http module provides methods like get, post, put, del, and batch for making HTTP requests. You can customize headers, request bodies, and query parameters just like you would in any HTTP client. The sleep function is crucial for pacing your requests; without it, k6 would hammer your API as fast as possible, which might not reflect real-world usage patterns.
The options object is where you define the overall test scenario. You can specify duration instead of iterations, and k6 will run for that amount of time, trying to achieve a certain rate of iterations per second. You can also define multiple executors with different stages to ramp up and down load gradually, simulating more complex user behavior over time.
A subtle but powerful feature is the ability to define custom metrics beyond the default ones. For example, you could track the number of specific types of responses or the time it takes to process a particular business logic step within your script. This is done using the k6/metrics module, allowing you to instrument your load tests with very granular performance data relevant to your application.
The next step is to move beyond simple GET requests and explore how to simulate more complex user flows, like logging in, performing actions, and logging out, all within a single k6 script.