Load Testing with k6, From Zero
- Shawn West
- Jul 30
- 3 min read
Updated: Aug 6
Performance Engineering · Part 5
Your app is fast for one user; the only way to know if it survives a thousand is to send a thousand at it before your users do. Load testing turns "I think it'll scale" into evidence. This walks through load-testing with k6 — writing a realistic scenario, ramping up traffic, setting pass/fail thresholds, and finding the point where the system starts to buckle.
Will the system handle 10x traffic? Load tests answer.
Step 1: Install k6 (3 min)
# macOS
brew install k6
# Docker
docker pull grafana/k6
Step 2: First Test (5 min)
// script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 10 virtual users
duration: '30s',
};
export default function () {
const res = http.get('https://test.example.com');
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
k6 run script.js
Output: requests per second, response times, error rate.
Step 3: Ramp-Up Pattern (5 min)
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp to 50
{ duration: '3m', target: 50 }, // Hold
{ duration: '1m', target: 100 }, // Ramp to 100
{ duration: '3m', target: 100 }, // Hold
{ duration: '1m', target: 0 }, // Ramp down
],
};
Realistic: traffic grows, sustains, grows more. Shows behavior at each level.
Step 4: Realistic Scenarios (10 min)
Real users don't just hit one endpoint:
export default function () {
// Step 1: sign in
let res = http.post('https://test.example.com/api/auth/login', {
email: 'load-test@example.com',
password: 'password',
});
const token = res.json('token');
// Step 2: browse
res = http.get('https://test.example.com/api/products', {
headers: { Authorization: `Bearer ${token}` },
});
// Step 3: view detail
const productId = res.json('products')[0].id;
http.get(`https://test.example.com/api/products/${productId}`, {
headers: { Authorization: `Bearer ${token}` },
});
sleep(2);
}
Models a real user session.
Step 5: Thresholds (5 min)
Set pass/fail criteria:
export const options = {
vus: 100,
duration: '5m',
thresholds: {
'http_req_duration{name:list_products}': ['p(95)<500'],
'http_req_failed': ['rate<0.01'],
},
};
Test fails if p95 latency > 500ms or error rate > 1%. CI-friendly.
Step 6: Custom Metrics (10 min)
import { Counter, Trend } from 'k6/metrics';
const failedSignups = new Counter('failed_signups');
const signupDuration = new Trend('signup_duration');
export default function () {
const start = Date.now();
const res = http.post(...);
signupDuration.add(Date.now() - start);
if (res.status !== 200) {
failedSignups.add(1);
}
}
Track business-level metrics, not just HTTP.
Step 7: Use Test Data (10 min)
Don't hammer with the same user:
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', function () {
return JSON.parse(open('./users.json'));
});
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
http.post('/api/login', { email: user.email, password: user.password });
}
Each VU picks random user. Realistic distribution.
Step 8: CI Integration (10 min)
# .github/workflows/load-test.yml
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * 1' # Weekly
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: grafana/setup-k6-action@v1
- run: k6 run script.js
Weekly load test. Catches performance regressions over time.
Step 9: Distributed Tests (advanced, 10 min)
For really high load, single machine isn't enough:
k6 Cloud (paid; massive scale)
k6 Operator (self-hosted distributed in Kubernetes)
Manual: run multiple k6 instances; aggregate results
Most need < 10k RPS = single laptop fine.
Step 10: Analyze Results (10 min)
After a run:
Response times: p50, p95, p99 — tail matters
Error rate: under threshold?
Throughput: requests/sec achieved
Resource saturation: CPU, memory, DB connections during the test
Identify the limiting factor. That's what scales first.
For visualization: k6 → InfluxDB → Grafana for rich dashboards.
What You Just Did
You can load-test your system. Realistic scenarios. Pass/fail thresholds. CI integration.
Common Failure Modes
Test from your laptop on a tiny network. Network is bottleneck; not the app.
Hammer one endpoint. Real users browse; realistic scenarios reveal more.
No baseline. Don't know what "good" looks like.
Run once. Should be regular; performance regressions are common.
Load test in production. Without coordination; affects real users.
Continue the Performance Engineering path
Previous — Part 4: Cache Hot Reads
Next — Part 6: Read Flame Graphs
Part of the Performance Engineering learning path.


