Lighthouse tests one user at a time. Load testing answers a different question: how does your application perform when hundreds or thousands of users hit it simultaneously? k6 is a modern, developer-friendly load testing tool that uses JavaScript for test scripts.
What Is Load Testing?
Load testing simulates concurrent users to measure how your system handles traffic. It reveals:
- Throughput: How many requests per second can your server handle?
- Latency under load: Do response times degrade as users increase?
- Breaking points: At what load does the system start failing?
- Resource bottlenecks: Is it CPU, memory, database connections, or network bandwidth?
Installing k6
k6 is a standalone binary — no Node.js runtime needed:
# macOS
brew install k6
# Windows (with Chocolatey)
choco install k6
# Docker
docker run --rm -i grafana/k6 run - < script.js
# Or download from https://k6.io/docs/get-started/installation/
Writing Your First Load Test
Create a file called load-test.js:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 10 virtual users
duration: '30s', // run for 30 seconds
};
export default function () {
const res = http.get('http://localhost:3000/');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1); // simulate user think time
}Run it:
k6 run load-test.jsEach virtual user (VU) executes the default function in a loop for the specified duration. The check() function validates response conditions, and sleep() simulates realistic user pauses between actions.
Ramp-Up Patterns
Real traffic does not arrive all at once. Use stages to simulate gradual increases:
export const options = {
stages: [
{ duration: '1m', target: 20 }, // ramp up to 20 users
{ duration: '3m', target: 20 }, // stay at 20 users
{ duration: '1m', target: 50 }, // ramp up to 50 users
{ duration: '3m', target: 50 }, // stay at 50 users
{ duration: '1m', target: 0 }, // ramp down to 0
],
};This pattern is called a "stepped load test." It helps you identify at what level of concurrency performance starts to degrade.
Other Common Patterns
- Spike test: Ramp to a very high number quickly, then drop. Tests how the system handles sudden traffic surges.
- Soak test: Run at moderate load for hours. Finds memory leaks and resource exhaustion over time.
- Stress test: Gradually increase beyond expected capacity to find the breaking point.
Thresholds
Thresholds define pass/fail criteria for your load test. If any threshold is breached, k6 exits with a non-zero code — perfect for CI:
export const options = {
vus: 50,
duration: '2m',
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // less than 1% failure rate
http_reqs: ['rate>100'], // at least 100 req/s throughput
checks: ['rate>0.99'], // 99% of checks pass
},
};The p(95) syntax means the 95th percentile — 95% of all requests should complete within the specified time. This is more meaningful than average response time because averages hide outliers.
Testing Multiple Endpoints
Real users do not just hit the homepage. Simulate a realistic scenario:
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
// Visit homepage
let res = http.get('http://localhost:3000/');
check(res, { 'home status 200': (r) => r.status === 200 });
sleep(2);
// Browse products
res = http.get('http://localhost:3000/api/products');
check(res, { 'products status 200': (r) => r.status === 200 });
sleep(1);
// View a product
res = http.get('http://localhost:3000/api/products/42');
check(res, { 'product detail 200': (r) => r.status === 200 });
sleep(3);
// Search
res = http.get('http://localhost:3000/api/search?q=keyboard');
check(res, { 'search status 200': (r) => r.status === 200 });
sleep(1);
}Reading Results
After a test run, k6 prints a summary with key metrics:
- http_req_duration: Response time statistics (avg, min, med, max, p90, p95, p99).
- http_reqs: Total number of requests and requests per second.
- http_req_failed: Percentage of failed requests.
- vus: Number of active virtual users over time.
- checks: How many checks passed versus failed.
For persistent storage and visualization, you can stream results to Grafana Cloud, InfluxDB, or a CSV file:
k6 run --out csv=results.csv load-test.jsKey Takeaways
- k6 simulates concurrent users to reveal how your system performs under load.
- Use
stagesfor realistic ramp-up patterns instead of constant load. - Set
thresholdsto enforce performance requirements and fail CI when they are breached. - Focus on p95 response times, not averages — they reveal the experience of your slowest users.
- Test multiple endpoints to simulate realistic user journeys.