Skip to main content
Performance Testing·Lesson 4 of 5

Load Testing with k6

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.js

Each 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.js

Key Takeaways

  • k6 simulates concurrent users to reveal how your system performs under load.
  • Use stages for realistic ramp-up patterns instead of constant load.
  • Set thresholds to 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.