You can run a load test in under 5 minutes with k6, but the real surprise is how little code it actually takes to generate meaningful load.
Here’s a basic script that hits http://localhost:8080 with 10 virtual users, each making a single HTTP GET request:
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 10, // Number of virtual users
duration: '30s', // Duration of the test
};
export default function () {
http.get('http://localhost:8080');
sleep(1); // Pause for 1 second between iterations
}
Save this as test.js. To run it, open your terminal in the same directory and execute:
k6 run test.js
You’ll see output like this, showing test duration, total requests, and average response time:
/\ | cyclohexane
/ \ |
/ \ | http_req_duration
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | http_req_failed
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | http_req_sending
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | http_req_waiting
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | http_req_connecting
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | http_req_tls_handshaking
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | http_req_blocked
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | data_received
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | data_sent
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | iteration_duration
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | vus
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
/\ | vus_max
/ \ |
/ \ |
/______\ |
/________\ |
/__________\ |
/____________\|
INFO[0000] Running test.js:00000000000000000000
INFO[0000] Starting test...
INFO[0000] Target URL: http://localhost:8080
INFO[0000] Executing HTTP GET request to http://localhost:8080
INFO[0000] Executing HTTP GET request to http://localhost:8080
INFO[0000] Executing HTTP GET request to http://localhost:8080
...
INFO[0030] Test finished
execution_time : 30.01s
scenarios in which the test was run : (main: 30s)
VU.max : 10
http_req_blocked : avg=0.01ms min=0ms med=0ms max=0.07ms p(90)=0ms p(99)=0ms
http_req_connecting : avg=0ms min=0ms med=0ms max=0ms p(90)=0ms p(99)=0ms
http_req_failed : 00.00% ✓ | rate=0/s min=0/s med=0/s max=0/s p(90)=0/s p(99)=0/s
http_req_receiving : avg=0.02ms min=0ms med=0ms max=0.07ms p(90)=0ms p(99)=0ms
http_req_sending : avg=0.01ms min=0ms med=0ms max=0ms p(90)=0ms p(99)=0ms
http_req_tls_handshaking: avg=0ms min=0ms med=0ms max=0ms p(90)=0ms p(99)=0ms
http_req_waiting : avg=0.04ms min=0ms med=0ms max=0.16ms p(90)=0ms p(99)=0ms
http_req_duration : avg=0.07ms min=0ms med=0ms max=0.23ms p(90)=0ms p(99)=0ms
http_req_failed : 00.00% ✓ | rate=0/s min=0/s med=0/s max=0/s p(90)=0/s p(99)=0/s
http_req_failed : 00.00% ✓ | rate=0/s min=0/s med=0/s max=0/s p(90)=0/s p(99)=0/s
data_received : 2.7 kB 0.09 kB/s
data_sent : 1.5 kB 0.05 kB/s
iteration_duration : avg=1.01s min=1s med=1s max=1.02s p(90)=1.02s p(99)=1.02s
vus : 10 min=10 current=10 max=10
vus_max : 10
[summary]
http_req_duration : avg=0.07ms min=0ms med=0ms max=0.23ms p(90)=0ms p(99)=0ms
http_req_failed : 00.00% ✓
data_received : 2.7 kB 0.09 kB/s
data_sent : 1.5 kB 0.05 kB/s
iteration_duration : avg=1.01s min=1s med=1s max=1.02s p(90)=1.02s p(99)=1.02s
vus : 10 min=10 current=10 max=10
The options object is your primary control panel. vus defines the number of concurrent virtual users, and duration sets how long the test runs. The default function is the code each virtual user executes. http.get() makes the request, and sleep(1) ensures each user waits one second before repeating the request. This simulates a user browsing, not just hammering a single endpoint.
To make this test meaningful, you need to represent your actual user flows. This means making multiple requests, checking responses, and varying the load. Here’s a more realistic example that simulates a user logging in, fetching data, and logging out:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50,
duration: '1m',
thresholds: {
'http_req_failed': 'rate<0.01', // http errors should be less than 1%
'http_req_duration': ['p(95)<500'], // 95% of requests must complete below 500ms
},
};
export default function () {
// Login
const loginRes = http.post('http://localhost:8080/login', {
username: 'testuser',
password: 'password123',
});
check(loginRes, {
'login successful': (r) => r.status === 200,
'login response body contains token': (r) => r.body.includes('token'),
});
const token = JSON.parse(loginRes.body).token;
// Fetch user data
const dataRes = http.get('http://localhost:8080/users/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
check(dataRes, {
'user data received': (r) => r.status === 200,
});
// Logout
http.post('http://localhost:8080/logout', null, {
headers: {
Authorization: `Bearer ${token}`,
},
});
sleep(Math.random() * 3 + 1); // Random delay between 1 and 4 seconds
}
This script introduces check() for assertions—verifying that requests are successful and return expected data. The thresholds in options define pass/fail criteria for the entire test run based on error rates and response times. The sleep now uses Math.random() to add variability, making the load more realistic.
The core idea is to model user behavior. If your users navigate through several pages, perform actions, and wait between actions, your k6 script should mirror that. This means chaining requests, passing data (like authentication tokens) between them, and using sleep to simulate user think time. The http module provides methods for GET, POST, PUT, DELETE, etc., and you can pass body and headers as needed.
The true power of k6 lies in its ability to script complex scenarios and define performance goals. You’re not just sending requests; you’re simulating user journeys. The check function is critical for ensuring that the correct behavior is occurring under load, not just that the server is responding. When you define thresholds, you’re setting explicit performance targets that k6 will validate.
What most people miss is how to effectively extract and use dynamic data from responses. For instance, after a login, you usually get a token. You need to parse the response body, extract that token, and then include it in the headers of subsequent requests. This is done using JSON.parse(response.body) and then accessing properties of the resulting JavaScript object. This dynamic data flow is what makes load tests truly representative of real-world application usage.
The next step is to explore k6’s scripting API for more advanced features like custom metrics, different protocol support, and integrating with CI/CD pipelines.