Load Testing Made Easy with Grafana K6
Simple to use, powerful in action
At River Island, I initially used Apache JMeter for performance testing, but switched to K6 when tasked with testing a new API endpoint. K6 proved easy to learn, script, and use, offering greater simplicity and power compared to other tools.
As I became confident, I integrated smoke load test into our CI/CD pipeline and introduced K6 to other engineers. The tool quickly gained adoption, with test engineers incorporating it into their workflows for load testing and to check response times.
This article serves as a quick guide to showcase how simple it is to learn the basics of Grafana K6 and start testing effectively. It follows the structure I used in my training session, designed for easy learning and use.
What is K6?
K6 is a modern load testing tool designed to test the performance of APIs, microservices, and websites. Developed primarily in Go (source code available here), K6 leverages JavaScript for scripting, offering an approachable and developer-friendly experience for both testers and engineers.
Use Cases
K6 is a versatile tool for various performance testing scenarios:
- Web Application Testing: Simulate different load conditions to measure response times, throughput, and concurrency.
- API Testing: Test RESTful APIs, GraphQL endpoints, or microservices for reliability, response times, and compliance with SLAs.
- E-commerce Load Testing: Validate platform resilience during high-traffic events like Black Friday by simulating user traffic.
- Mobile App Testing: Assess API response times, latency, and performance under different network conditions.
Benefits of Using K6
Key features that make K6 stand out:
- Flexible Scripting: Write test scripts in JavaScript to simulate real-world scenarios with custom logic and dynamic data.
- Rich Metrics Collection: Gather detailed performance data like response times, error rates, and throughput to identify bottlenecks.
- Thresholds and Assertions: Automate tests with custom pass/fail conditions, such as maximum response times or error rates.
- Real-time Results: Visualize test metrics in real time using Grafana Cloud for quick analysis and iterative improvements.
Load Test Types
Before we explore the structure and example tests, I want to briefly cover load testing types
Key Terms
To understand performance testing better, here are some key terms:
Installation
Installing K6 is straightforward.
For macOS:
Simply run the following command at the terminal using Homebrew
brew install k6
For Linux or Windows:
Follow the official installation guide on the K6 website.
After installation, the K6 binary should have been added to your system’s PATH automatically. You can verify the installation by running:
k6 version
which should result something like
Where:
- k6 version shows the installed K6 version.
- go1.22.1 indicates the Go version K6 was built with.
- darwin/amd64 specifies the OS and machine architecture.
Structure of a K6 Test
Each K6 script is written in JavaScript and typically follows a structured format, divided into four key sections. Here’s a breakdown, I have numbered them
- Init Code (Required): This section always runs first and is mandatory for setting up imports, libraries, and global variables.
- Setup Code (Optional): Use this to initialize prerequisites. It runs only once before the test starts.
- VU Code (Required): The
defaultfunction is the heart of the test, executed by each virtual user. - Teardown Code (Optional): Runs after the test is complete, handling cleanup tasks like closing connections or freeing resources.
NOTE: K6 is a powerful tool. Please use it responsibly. Avoid testing on systems you don’t own or have explicit permission to test, as this may lead to IP blocking or other unintended consequences.
Example Tests
Let’s dive into some hands-on examples. For simplicity, I’ll focus on different scenarios and corresponding tests. To make this approachable, I’ll use the K6 test site: https://test.k6.io/.
NOTE: It should be simple to follow but basic Javascript knowledge might help
Scenario 1 — Simple GET Call
Since it is the first test, I have put multiple steps for this scenario.
Step 1: Create a Test File
Start by creating a new JavaScript file to hold your test script. This will be the file you run with the k6 command. For example, create a file named test-one.js in your preferred working directory using your favourite code editor (e.g., Visual Studio Code or any text editor) to start writing the test script.
Step 2: Import K6 Modules (Init Code)
In the init code section, you import the modules and libraries required for the test. For a simple GET test, you’ll need the http module. The http module provides methods for making HTTP requests like GET, POST, PUT, etc.
// 1. Init code
import http from 'k6/http';The init code is executed only once when the test starts, regardless of the number of virtual users or iterations.
Step 3: Define the Default Function (VU Code)
Now define the default exported function. The default exported function is the entry point for the test script. It will be executed repeatedly the number of times you define with the iterations option.
Here’s the final code of the file:
// 1. Init code
import http from 'k6/http'; // importing http module
// 2. VU code
export default function () {
// performing GET call
http.get('https://test.k6.io/');
}Step 4: Run the Test
Navigate to the directory where your test file test-one.js is located. Then, execute the following command in your terminal:
k6 run test-one.jsAfter running the test, K6 provides a detailed summary of the test execution
I know there are lot of metrics to go through but let’s just focus on key metric http_req_duration
The http_req_duration metric is crucial for evaluating the performance of your HTTP requests. This metric provides detailed insights into how quickly your system responds to requests under the given load. Here's a breakdown:
Get Srinivas Nali’s stories in your inbox
Join Medium for free to get updates from this writer.
avg (Average)
- The average time it took for all HTTP requests to complete.
- In this case, the average response time is 90.75ms, indicating how the system performs overall.
min and max
- Minimum (min): The fastest response time observed during the test (85.52ms).
- Maximum (max): The slowest response time observed (95.99ms).
- These values help you understand the range of response times and spot extreme cases.
Percentiles (p(90) and p(95))
- p(90): 90% of the requests were completed in 94.94ms or less.
- p(95): 95% of the requests were completed in 95.47ms or less.
Percentiles are useful for analyzing the distribution of response times, highlighting how the majority of requests perform.
Some useful Flags
Below are some useful flags that you can use with the k6 run command to customise the test execution:
Scenario 2 — Test with Assertion
In this scenario, we will create a test file called test-two.js where assertions are used to verify that specific conditions hold true. Below is the code example:
// 1. Init code
import http from 'k6/http';
import { check } from 'k6'; // importing another module 'check' for assertions
// 2. VU code
export default function () {
const res = http.get('http://test.k6.io/');
// asserting the response code
check(res, {
'status should be 200': (r) => r.status === 200,
});
}Explanation to the code
checkFunction: This is used to define assertions for your test. Each assertion checks if a specific condition is true.- Assertion Description:
'status should be 200'is a human-readable label for the check. - Callback Function:
(r) => r.status === 200ensures the response's status is 200.
Since the goal of this article is to focus on the basics of the K6 tool, I am not delving into the concept of arrow function expression, i.e.
=>. You can refer to this link for more details.
When you run the test using k6 run test-two.js results summary should be some thing like this
Check Results
✓ status should be 200
checks.........................: 100.00% ✓ 1 ✗ 0- The check, “status should be 200”, passed.
- 100% success rate: 1 successful check, 0 failed checks.
http_req_duration:
- avg=86.41ms: Average time to complete an HTTP request.
- min=83.75ms, max=89.07ms: Range of request durations.
- p(90)=88.53ms, p(95)=88.8ms: 90% and 95% of requests completed within ~88ms.
Scenario 3 — Test with Thresholds & Assertion
In this scenario, let’s add a threshold to ensure that certain metrics stay within acceptable limits. Below is the test-three.js file code:
// 1. Init code
import http from 'k6/http';
import { check } from 'k6'
export const options = {
// Setting thresholds for response duration
thresholds: {
http_req_duration : ['p(90)<100','p(95)<100']
}
}
// 2. VU code
export default function () {
const res = http.get('http://test.k6.io/');
check (res, {
'response code is 200': (r) => r.status === 200
});
}options:
- This is the block we define
thresholds, in this example we are setting thresholds on particular metrichttp_req_duration. 'p(90)<100'ensures that 90% of the requests have a duration below 100ms.'p(95)<100'ensures that 95% of the requests have a duration below 100ms.
The
optionsobject in a K6 test script is used to configure test execution. It defines how the test should run, including settings for virtual users (VUs), duration, thresholds, and scenarios. Refer this official page for more information on options block
When you run the test using k6 run test-three.js results summary should be some thing like this, I have highlighted the values, they are within the defined thresholds
If thresholds are changed in the test to 80 like this
// 1. Init code
import http from 'k6/http';
import { check } from 'k6'
export const options = {
// Setting thresholds for response duration to 80
thresholds: {
http_req_duration : ['p(90)<80','p(95)<80']
}
}
// 2. VU code
export default function () {
const res = http.get('http://test.k6.io/');
check (res, {
'response code is 200': (r) => r.status === 200
});
}and run the test, the test result should be Failure like this, since the thresholds are not met
- The p(90) and p(95) thresholds weren’t met as expected since the threshold was set to be less than 80, but the actual thresholds are above 80, as highlighted.
Scenario 4 — Test with Thresholds, Assertion & VUs
In this scenario, let’s aim to validate the system’s performance by using multiple virtual users (VUs) and defining thresholds for both response times and failure rates. The vus option specifies that 5 virtual users will simulate concurrent traffic, while the duration sets the test to run for 2 seconds. Lets create a new test file called test-four.js with the below code
// 1. Init code
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 5, // Number of virtual users to simulate
duration: '2s', // Total duration of the test (in seconds)
thresholds: {
http_req_failed: ['rate<0.01'], // Threshold for failure rate (less than 1%)
http_req_duration: ['p(95)<1000'], // 95th percentile of request duration should be below 1 second
},
};
// 2. VU code
export default function () {
const res = http.get('http://test.k6.io/');
check(res, {
'response code is 200': (r) => r.status === 200,
});
}when the test is run using the command k6 run test-four.js the result should be some thing like this
The test executed successfully with 5 Virtual Users (VUs) running for 2 seconds. Here’s a quick breakdown of the key result metrics:
Check Results
- The assertion
response code is 200passed for all requests. - Success rate: 100% (✓ 51 checks passed, ✗ 0 failed).
HTTP Requests:
- Total: 102 requests with a rate of ~46.7 requests/second.
- No failed requests (http_req_failed: 0.00%).
Response Duration (http_req_duration) Percentiles:
- 90% of requests completed in less than 97.47ms.
- 95% of requests completed in less than 103.73ms.
Scenario 5 — Load Test with stages
In this last scenario, let’s see how a load test can be done with multiple stages to simulate real-world traffic patterns, including ramp-up, constant load, and ramp-down phases.
create a file with name test-five.js with below code
// 1. Init code
import http from 'k6/http';
export const options = {
stages: [
{ duration: '30s', target: 2 }, // Ramp-up load to 2 users in 30 seconds
{ duration: '4m', target: 5 }, // Maintain constant load with 5 users for 4 minutes
{ duration: '30s', target: 0 }, // Ramp-down to 0 users in 30 seconds
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<2000'], //less than 2 seconds
},
};
// 2. VU code
export default function () {
const res = http.get('http://test.k6.io/');
}The stages option configures the test's traffic pattern:
- Ramp-Up Stage: Gradually increase the load to 2 users over 30 seconds.
- Constant Load Stage: Sustain a steady load of 5 users for 4 minutes.
- Ramp-Down Stage: Gradually decrease the load back to 0 users over 30 seconds.
when the test is run using the command the result should be something like this
lets have a quick look on key metrics from the above screen shot
Duration (http_req_duration):
- Average:
100.11ms - Median (50th percentile):
93.18ms - 90th Percentile:
105.22ms - 95th Percentile:
121.51ms - Min/Max:
81.54ms / 1.12s
Iterations:4144 iterations, ~13.81 iterations/second.
Duration per Iteration:
- Average:
202.73ms - Median:
187.79ms - 90th Percentile:
219.1ms - 95th Percentile:
298.78ms
The response times remained well within the defined thresholds (p(95)<2000ms).
The ramp-up, constant load, and ramp-down stages showcased stable performance, indicating readiness for scaling.
K6 has many more great features, and what I’ve covered here is just a small part of its capabilities. However, I hope this article helps you kickstart with the basics.
References
I recommend checking the official resources for more in-depth understanding:
Best Practices for Load Testing
I want to emphasize that load testing is not just about pushing systems to their limits; it’s about understanding their behavior under realistic scenarios. A structured approach ensures reliable results and adds real value. With that in mind, here are the essential guidelines to follow:
- Have a Clear Test Plan: Define the scope, goals (e.g., stability, scalability), and target endpoints.
- Understand the System: Analyze architecture, peak traffic, load duration, and any rate limits or constraints.
- Inform Stakeholders: Notify relevant teams about the testing schedule and potential impacts, like slower response times.
- Avoid Testing on Production: Test in a dedicated environment to prevent disruptions to live systems.
- Start Small, Scale Gradually: Begin with a low load to establish a baseline, then incrementally increase load.
- Analyze and Present Results: Use dashboards or reports to track response times, resource usage, and error rates, and share insights and improvement recommendations.
My boss once told me that my role as a principal test engineer is like a torchbearer, guiding others through the journey of knowledge. I’m grateful that I was able to learn K6 and share that knowledge with my fellow test engineers.
Happy Testing!!









