top of page

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

Part of the Performance Engineering learning path.

bottom of page