top of page

Load Testing with k6, From Zero

S. West
Jul 30
3 min read

Updated: 4 days ago

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