Load testing APIs with k6 for performance validation

Ryan Nakamura Feb 2026
1 tab
// k6 Load Test Configuration
// Run: k6 run load-test.js --env BASE_URL=https://api.example.com

import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';

// Custom metrics
const errorRate = new Rate('errors');
const apiDuration = new Trend('api_duration', true);
const successfulLogins = new Counter('successful_logins');

// Test configuration with multiple scenarios
export const options = {
  scenarios: {
    // Scenario 1: Smoke test (sanity check)
    smoke: {
      executor: 'constant-vus',
      vus: 1,
      duration: '30s',
      startTime: '0s',
      tags: { test_type: 'smoke' },
    },

    // Scenario 2: Load test (normal traffic)
    load: {
      executor: 'ramping-vus',
      startTime: '30s',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },   // Ramp up to 50 users
        { duration: '5m', target: 50 },   // Stay at 50 for 5 min
        { duration: '2m', target: 100 },  // Ramp up to 100
        { duration: '5m', target: 100 },  // Stay at 100 for 5 min
        { duration: '2m', target: 0 },    // Ramp down
      ],
      tags: { test_type: 'load' },
    },

    // Scenario 3: Spike test
    spike: {
      executor: 'ramping-vus',
      startTime: '17m',
      startVUs: 0,
      stages: [
        { duration: '10s', target: 500 },  // Sudden spike
        { duration: '1m', target: 500 },   // Hold spike
        { duration: '10s', target: 0 },    // Quick ramp down
      ],
      tags: { test_type: 'spike' },
    },
  },

  // Performance thresholds (fail test if not met)
  thresholds: {
    http_req_duration: [
      'p(95)<500',     // 95% of requests under 500ms
      'p(99)<1500',    // 99% of requests under 1.5s
    ],
    http_req_failed: ['rate<0.01'],   // Error rate under 1%
    errors: ['rate<0.05'],             // Custom error rate under 5%
    api_duration: ['avg<300'],          // Average API call under 300ms
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';

// Setup: runs once before the test
export function setup() {
  const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
    email: 'loadtest@example.com',
    password: 'test-password',
  }), { headers: { 'Content-Type': 'application/json' } });

  const token = loginRes.json('token');
  return { token };
}

// Main test function: runs for each VU iteration
export default function (data) {
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${data.token}`,
  };

  group('API Health', () => {
    const res = http.get(`${BASE_URL}/api/health`);
    check(res, {
      'health check status is 200': (r) => r.status === 200,
      'health check response time < 100ms': (r) => r.timings.duration < 100,
    });
    errorRate.add(res.status !== 200);
  });

  group('List Resources', () => {
    const res = http.get(`${BASE_URL}/api/snips?page=1&per_page=20`, { headers });
    const success = check(res, {
      'list status is 200': (r) => r.status === 200,
      'list returns array': (r) => Array.isArray(r.json('data')),
      'list response time < 500ms': (r) => r.timings.duration < 500,
    });
    apiDuration.add(res.timings.duration);
    errorRate.add(!success);
  });

  group('Get Single Resource', () => {
    const res = http.get(`${BASE_URL}/api/snips/1`, { headers });
    check(res, {
      'get status is 200': (r) => r.status === 200,
      'response has title': (r) => r.json('title') !== undefined,
    });
    apiDuration.add(res.timings.duration);
    errorRate.add(res.status !== 200);
  });

  group('Search', () => {
    const queries = ['docker', 'kubernetes', 'terraform', 'nginx'];
    const query = queries[Math.floor(Math.random() * queries.length)];
    const res = http.get(`${BASE_URL}/api/search?q=${query}`, { headers });
    check(res, {
      'search status is 200': (r) => r.status === 200,
      'search returns results': (r) => r.json('data').length >= 0,
      'search response time < 800ms': (r) => r.timings.duration < 800,
    });
    apiDuration.add(res.timings.duration);
  });

  sleep(Math.random() * 2 + 1);  // Think time: 1-3 seconds
}

// Teardown: runs once after the test
export function teardown(data) {
  console.log('Load test complete. Review results in Grafana or CI output.');
}
1 file · javascript Explain with highlit

Write comprehensive load tests using k6 to validate API performance before production deployments. Define scenarios with ramping VUs, set thresholds for response times and error rates, test specific endpoints, and integrate results with CI/CD pipelines for automated performance gates.