The 2 AM Phone Call and a Costly Lesson
You’re fast asleep when your phone rings incessantly. The dashboard is glowing red: server CPU is hitting 100%, database connections are bottlenecked, and users are complaining about the app hanging. After three hours of bleary-eyed debugging, you find the culprit: a new API deployed yesterday. With just 50 concurrent users, the entire system collapsed.
This isn’t a horror movie script; it’s a reality I’ve lived through. My biggest mistake then was focusing only on Unit Tests for logic while neglecting performance testing (Load Testing). To prevent such disasters, k6 is the powerful tool I want to introduce to you all.
Why k6?
Forget the clunky, heavy UIs of tools like JMeter. k6 allows you to write test scripts in JavaScript. This is incredibly convenient: you can manage test code directly in your repository, utilize version control, and leverage your existing coding skills.
1. Installing k6 in a Snap
For those on Linux (Ubuntu/Debian), just a few familiar commands will do:
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
macOS users have it even faster with Homebrew: brew install k6.
2. Your First Script: Hello API
Let’s try creating a test.js file to send requests to a product API:
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
http.get('https://api.example.com/products');
sleep(1);
}
Run the command: k6 run test.js. Simple as that—you’ve just performed 1 request per second. But in reality, systems need to handle much more complex loads than this.
Upgrading k6 into a Load-Handling “Slayer”
To simulate thousands of Virtual Users (VUs) accessing simultaneously, we need to configure Stages. Instead of typing long command-line parameters, I prefer putting the configuration directly into the code for better management and reusability.
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up to 20 users over the first 30 seconds (warm-up)
{ duration: '1m', target: 20 }, // Maintain 20 users for 1 minute to check stability
{ duration: '10s', target: 0 }, // Ramp down to 0 for a clean finish
],
};
This configuration helps simulate real-world behavior: traffic usually grows gradually rather than hitting all at once in a single second.
Setting Safety Margins (Checks and Thresholds)
Just seeing “Finished” on the screen isn’t enough. You need to know if the API returns a 200 status code and if the response time meets business requirements.
import { check } from 'k6';
let res = http.get('https://api.example.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'p95 response time < 500ms': (r) => r.timings.duration < 500,
});
Especially important are Thresholds. You can specify that if 10% of requests fail or 95% of requests (p95) are slower than 1 second, k6 will mark the test as failed. This is the final safeguard preventing buggy code from reaching production.
Simulating Real Scenarios: Auth and Data Flow
Most APIs today require authentication. Here is how I handle the login flow to get a token before accessing data:
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
const loginRes = http.post('https://api.example.com/login', {
username: 'admin',
password: 'password123',
});
const token = loginRes.json('token');
const params = { headers: { Authorization: `Bearer ${token}` } };
const res = http.get('https://api.example.com/private/data', params);
check(res, { 'is status 200': (r) => r.status === 200 });
// Simulate a user pausing to read content for about 1-4 seconds
sleep(Math.random() * 3 + 1);
}
Pro tip: When dealing with complex JSON or needing to quickly format data while writing scripts, I often use toolcraft.app. It saves much more time than fiddling with VS Code extensions.
Integrating Load Testing into the CI/CD Pipeline
Don’t wait until you have free time to run tests. Integrate them into GitHub Actions so that every time you push code, the system automatically checks performance. If new code slows down the API, the build will fail immediately.
name: Performance Check
on: [push]
jobs:
k6_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run k6
uses: grafana/[email protected]
with:
filename: test.js
Real-World Experience in Load Testing
- Never test on Production: No matter how confident you are, set up a Staging environment. You don’t want to accidentally DDoS your own system.
- Closely Monitor the Database: API code can be optimized, but 90% of bottlenecks lie in queries missing indexes. Enable slow query logs when running k6 to find the culprit.
- Use Realistic Data: Don’t test with the same single user repeatedly. Use a CSV file containing 1,000 different users to prevent the database from caching results, which causes artificial performance numbers.
- Observe the Error Rate: Sometimes a system returns HTTP 200 but the body reports a logic error. You must check the returned content carefully to ensure accuracy.
With just 30 minutes of k6 setup, you can sleep soundly. No more worrying about being woken up in the middle of the night, no more sudden server crashes. Good luck building truly robust systems!

