Web Application Load Testing with k6

k6 is a modern load testing tool enabling developers and DevOps engineers to test web applications and APIs at scale using JavaScript-based test scripts. With built-in support for realistic traffic patterns, custom metrics, and detailed performance reporting, k6 transforms load testing from a specialist task into an accessible, developer-friendly process. This guide covers k6 installation, script development, and performance testing strategies.

Table of Contents

  1. k6 Installation and Setup
  2. Basic Load Testing
  3. k6 Script Development
  4. Virtual User Scenarios
  5. Thresholds and Checks
  6. Performance Metrics
  7. HTML Reporting
  8. Conclusion

k6 Installation and Setup

Installing k6

# Ubuntu/Debian installation
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install -y k6

# CentOS/RHEL installation
sudo yum install -y https://dl.k6.io/rpm/repo.rpm
sudo yum install -y k6

# Or from source
wget https://github.com/grafana/k6/releases/download/v0.45.0/k6-v0.45.0-linux-amd64.tar.gz
tar xzf k6-v0.45.0-linux-amd64.tar.gz
sudo mv k6-v0.45.0-linux-amd64/k6 /usr/local/bin/

# Verify installation
k6 --version

First Test

# Run simple HTTP request test
k6 run --vus 1 --duration 10s - <<EOF
import http from 'k6/http';

export default function() {
  http.get('http://example.com');
}
EOF

# Parameters:
# --vus: Virtual users
# --duration: Test duration

Basic Load Testing

Simple Load Test Script

cat > example_test.js <<'EOF'
import http from 'k6/http';
import { sleep } from 'k6';

export default function() {
  http.get('http://example.com/api/users');
  sleep(1);
}
EOF

# Run with 10 VUs for 30 seconds
k6 run --vus 10 --duration 30s example_test.js

# Expected output shows:
# http_reqs: Request count
# http_req_duration: Response time
# http_req_failed: Failed requests
# Checks: Custom validation metrics

Ramping Load Pattern

# Script with ramping VU pattern
cat > ramp_test.js <<'EOF'
import http from 'k6/http';
import { sleep } from 'k6';

export let options = {
  stages: [
    { duration: '30s', target: 10 },   // Ramp to 10 VUs
    { duration: '1m30s', target: 50 }, // Ramp to 50 VUs
    { duration: '2m', target: 100 },   // Ramp to 100 VUs
    { duration: '1m', target: 0 },     // Ramp down to 0 VUs
  ],
};

export default function() {
  http.get('http://example.com/api/users');
  sleep(1);
}
EOF

k6 run ramp_test.js

Spike Testing

# Sudden traffic spike scenario
cat > spike_test.js <<'EOF'
import http from 'k6/http';

export let options = {
  stages: [
    { duration: '10s', target: 100 },  // Normal load
    { duration: '1s', target: 1000 },  // Spike!
    { duration: '10s', target: 100 },  // Back to normal
    { duration: '1s', target: 0 },     // Stop
  ],
};

export default function() {
  http.get('http://example.com/api/data');
}
EOF

k6 run spike_test.js

k6 Script Development

Making HTTP Requests

cat > http_requests.js <<'EOF'
import http from 'k6/http';

export default function() {
  // GET request
  let res = http.get('http://example.com/api/users');
  console.log(`Response status: ${res.status}`);

  // POST request with payload
  const payload = JSON.stringify({
    name: 'Test User',
    email: '[email protected]',
  });
  
  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };
  
  http.post('http://example.com/api/users', payload, params);

  // PUT/PATCH request
  http.put('http://example.com/api/users/1', payload, params);

  // DELETE request
  http.del('http://example.com/api/users/1');
}
EOF

k6 run http_requests.js

Request Validation

cat > validate_requests.js <<'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';

export default function() {
  const res = http.get('http://example.com/api/users');
  
  // Validate response
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'content type correct': (r) => r.headers['Content-Type'].includes('application/json'),
    'response contains data': (r) => r.body.includes('users'),
  });
  
  sleep(1);
}
EOF

k6 run validate_requests.js

Dynamic Variables and Parameterization

cat > parameterized_test.js <<'EOF'
import http from 'k6/http';
import { sleep } from 'k6';

const BASE_URL = 'http://example.com';
const USER_IDS = ['1', '2', '3', '4', '5'];

export default function() {
  // Random user selection
  const userId = USER_IDS[Math.floor(Math.random() * USER_IDS.length)];
  
  const res = http.get(`${BASE_URL}/api/users/${userId}`);
  console.log(`Fetched user: ${userId}`);
  
  sleep(Math.random() * 3 + 1); // Random sleep 1-4 seconds
}
EOF

k6 run parameterized_test.js

Virtual User Scenarios

Multi-Scenario Test

cat > multi_scenario.js <<'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = 'http://example.com';

export let options = {
  scenarios: {
    // Browse scenario: Light usage
    browse: {
      executor: 'constant-vus',
      vus: 10,
      duration: '3m',
      env: { SCENARIO: 'browse' },
    },
    // Search scenario: Medium load
    search: {
      executor: 'ramping-vus',
      stages: [
        { duration: '1m', target: 20 },
        { duration: '3m', target: 20 },
        { duration: '1m', target: 0 },
      ],
      env: { SCENARIO: 'search' },
    },
    // Spike scenario: Sudden load
    spike: {
      executor: 'variable-looping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 100 },
        { duration: '1m', target: 0 },
      ],
      env: { SCENARIO: 'spike' },
    },
  },
};

export default function() {
  const scenario = __ENV.SCENARIO;
  
  if (scenario === 'browse') {
    http.get(`${BASE_URL}/api/users`);
  } else if (scenario === 'search') {
    http.get(`${BASE_URL}/api/search?q=test`);
  } else if (scenario === 'spike') {
    http.post(`${BASE_URL}/api/orders`, JSON.stringify({ items: [] }));
  }
  
  sleep(1);
}
EOF

k6 run multi_scenario.js

Thresholds and Checks

Setting Performance Thresholds

cat > thresholds_test.js <<'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
  vus: 10,
  duration: '1m',
  thresholds: {
    // HTTP request duration thresholds
    'http_req_duration': [
      'p(95) < 500',  // 95% of requests < 500ms
      'p(99) < 1000', // 99% of requests < 1000ms
      'max < 2000',   // No request > 2000ms
    ],
    // HTTP request failure threshold
    'http_req_failed': [
      'rate < 0.1',   // Less than 10% failure rate
    ],
    // Custom check threshold
    'checks': [
      'rate > 0.95',  // 95%+ checks pass
    ],
  },
};

export default function() {
  const res = http.get('http://example.com/api/users');
  
  check(res, {
    'status 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
  
  sleep(1);
}
EOF

k6 run thresholds_test.js

# Failed thresholds cause non-zero exit code
echo "Exit code: $?"

Performance Metrics

Custom Metrics

cat > custom_metrics.js <<'EOF'
import http from 'k6/http';
import { Counter, Trend, Rate, Gauge } from 'k6/metrics';

// Define custom metrics
const myCounter = new Counter('my_counter');
const myTrend = new Trend('my_trend');
const myRate = new Rate('my_rate');
const myGauge = new Gauge('my_gauge');

export default function() {
  const res = http.get('http://example.com/api/users');
  
  // Record metrics
  myCounter.add(1);                           // Increment counter
  myTrend.add(res.timings.duration);          // Track duration trend
  myRate.add(res.status === 200);             // Track success rate
  myGauge.add(Math.random() * 100);           // Set gauge value
}
EOF

k6 run custom_metrics.js

# View metrics in output
# my_counter.......................: X
# my_trend..........................avg=Yms
# my_rate...........................X%
# my_gauge..........................X

Detailed Metrics Report

# Run test with JSON output
k6 run --out json=results.json http_requests.js

# Analyze results with jq
cat results.json | jq '.data.samples[] | select(.metric=="http_req_duration") | .value' | \
  jq -s 'add/length'  # Calculate average

# Summary statistics
cat results.json | jq '.data.samples | group_by(.metric) | map({metric: .[0].metric, samples: length})'

HTML Reporting

Generating HTML Reports

# Install k6-reporter
npm install -g @kevinsullivan/k6-reporter

# Run test and generate report
k6 run --out json=results.json multi_scenario.js

# Convert to HTML
k6-reporter results.json --title "Load Test Report"

# View report
open summary.html

Grafana Cloud Integration

cat > grafana_test.js <<'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
  vus: 5,
  duration: '5m',
  ext: {
    loadimpact: {
      projectID: 12345,        // Your Grafana Cloud project ID
      name: 'API Load Test',
    },
  },
};

export default function() {
  const res = http.get('http://example.com/api/users');
  
  check(res, {
    'status is 200': (r) => r.status === 200,
  });
  
  sleep(1);
}
EOF

# Run against Grafana Cloud
export GRAFANA_CLOUD_API_TOKEN="your-token"
k6 cloud grafana_test.js

Conclusion

k6 modernizes load testing through developer-friendly scripting, making performance validation accessible to entire development teams. By implementing realistic scenarios, setting meaningful thresholds, and continuously validating application performance under load, organizations identify bottlenecks before they impact users. Integration with CI/CD pipelines enables performance regression detection, while Grafana Cloud integration provides persistent monitoring capabilities. Whether conducting pre-deployment validation, capacity planning, or ongoing performance monitoring, k6's combination of ease-of-use and powerful capabilities makes it the modern standard for web application load testing.