SuperAPI AI Directory

load-testing

Testing & quality

View repository

Updated 10-19 20:09

About this skill

Load Testing

You are now operating in load and performance testing mode.

Tool Selection

  • k6: JavaScript-based, excellent for complex scenarios, great for APIs and web apps
  • vegeta: Go-based, simple CLI, ideal for constant-rate HTTP load testing

k6 — JavaScript Load Testing

Installation

# macOS (via Homebrew)
brew install k6
Linux (Debian/Ubuntu)

sudo gpg -k sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update && sudo apt-get install k6

Via Docker

docker run --rm grafana/k6 run - <script.js

Verify installation

k6 version


### Basic k6 Script

```javascript
// script.js
import http from 'k6/http'
import { sleep, check } from 'k6'

export const options = {
  vus: 10,           // virtual users
  duration: '30s',   // test duration
}

export default function () {
  const res = http.get('http://localhost:8080/api/health')

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  })

  sleep(1)  // think time between requests
}
# Run the script
k6 run script.js

# Run with more virtual users
k6 run --vus 50 --duration 60s script.js

# Run with output to JSON
k6 run --out json=results.json script.js

Ramp-Up Patterns

// Gradual ramp-up (recommended for production load tests)
export const options = {
  stages: [
    { duration: '1m', target: 20 },   // ramp up to 20 VUs over 1 minute
    { duration: '3m', target: 20 },   // hold at 20 VUs for 3 minutes
    { duration: '1m', target: 50 },   // ramp up to 50 VUs
    { duration: '3m', target: 50 },   // hold at 50 VUs
    { duration: '1m', target: 0 },    // ramp down to 0
  ],
}

// Spike test (sudden traffic burst)
export const options = {
  stages: [
    { duration: '1m', target: 10 },   // warm up
    { duration: '30s', target: 200 }, // spike to 200 VUs
    { duration: '1m', target: 10 },   // recover
    { duration: '1m', target: 0 },    // ramp down
  ],
}

// Stress test (find breaking point)
export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '2m', target: 200 },
    { duration: '2m', target: 300 },
    { duration: '2m', target: 400 },
    { duration: '2m', target: 0 },
  ],
}

// Soak test (extended duration at expected load)
export const options = {
  stages: [
    { duration: '5m', target: 50 },   // ramp up
    { duration: '4h', target: 50 },   // hold for 4 hours
    { duration: '5m', target: 0 },    // ramp down
  ],
}

Threshold Configuration

// Define pass/fail criteria
export const options = {
  vus: 50,
  duration: '2m',
  thresholds: {
    // 95th percentile response time must be below 500ms
    http_req_duration: ['p(95)<500'],

    // 99th percentile must be below 1 second
    'http_req_duration{expected_response:true}': ['p(99)<1000'],

    // Error rate must be below 1%
    http_req_failed: ['rate<0.01'],

    // Custom metric threshold
    'checks{check:status is 200}': ['rate>0.99'],

    // Multiple thresholds for same metric
    http_req_duration: ['p(90)<300', 'p(95)<500', 'p(99)<1000'],
  },
}

POST Requests and Authentication

import http from 'k6/http'
import { check, sleep } from 'k6'

const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'
const API_TOKEN = __ENV.API_TOKEN

export const options = {
  stages: [
    { duration: '30s', target: 20 },
    { duration: '1m', target: 20 },
    { duration: '30s', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed: ['rate<0.05'],
  },
}

export default function () {
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${API_TOKEN}`,
  }

  // POST request
  const payload = JSON.stringify({
    title: 'Test item',
    description: 'Load test payload',
  })

  const createRes = http.post(`${BASE_URL}/api/items`, payload, { headers })
  check(createRes, {
    'item created': (r) => r.status === 201,
    'has item id': (r) => r.json('id') !== undefined,
  })

  if (createRes.status === 201) {
    const id = createRes.json('id')

    // GET request with the created ID
    const getRes = http.get(`${BASE_URL}/api/items/${id}`, { headers })
    check(getRes, {
      'item found': (r) => r.status === 200,
    })
  }

  sleep(1)
}

Custom Metrics

import { Trend, Counter, Rate, Gauge } from 'k6/metrics'

const loginDuration = new Trend('login_duration')
const loginErrors = new Counter('login_errors')
const loginSuccessRate = new Rate('login_success')
const activeUsers = new Gauge('active_users')

export default function () {
  const start = Date.now()

  const res = http.post('/api/auth/login', { ... })

  loginDuration.add(Date.now() - start)

  if (res.status !== 200) {
    loginErrors.add(1)
    loginSuccessRate.add(false)
  } else {
    loginSuccessRate.add(true)
  }

  activeUsers.add(1)
}

Running k6

# Basic run
k6 run script.js

# With environment variables
k6 run --env BASE_URL=https://staging.example.com --env API_TOKEN=$TOKEN script.js

# With output to various sinks
k6 run --out json=results.json script.js
k6 run --out influxdb=http://localhost:8086/k6 script.js
k6 run --out csv=results.csv script.js

# Run with quiet mode (less output)
k6 run --quiet script.js

# Abort on first threshold breach
k6 run --abort-on-fail script.js

# Run with tags (for filtering results)
k6 run --tag environment=staging script.js

# List results summary
k6 run script.js | tee load-test-results.txt

vegeta — HTTP Load Testing Tool

Installation

# macOS (via Homebrew)
brew install vegeta

# Download binary from GitHub releases
# https://github.com/tsenart/vegeta/releases

# Verify installation
vegeta -version

Basic Usage

# Simple constant-rate attack
echo "GET http://localhost:8080/api/health" | \
  vegeta attack -rate=100 -duration=30s | \
  vegeta report

# Rate: 100 requests per second for 30 seconds

# Attack with specific duration
echo "GET http://localhost:8080/" | \
  vegeta attack -rate=50 -duration=60s | \
  tee results.bin | \
  vegeta report

vegeta Targets File

# targets.txt — define requests
GET http://localhost:8080/api/health
Authorization: Bearer mytoken

GET http://localhost:8080/api/users

POST http://localhost:8080/api/items
Content-Type: application/json
@body.json
# body.json
# {"title": "test", "description": "load test"}

# Run with targets file
vegeta attack -targets=targets.txt -rate=100 -duration=30s | \
  vegeta report

vegeta Output Formats

# Text report (default)
vegeta attack -rate=100 -duration=30s -targets=targets.txt | \
  vegeta report

# JSON report
vegeta attack -rate=100 -duration=30s -targets=targets.txt | \
  vegeta report -type=json > report.json

# Histogram report
vegeta attack -rate=100 -duration=30s -targets=targets.txt | \
  vegeta report -type=hdrplot > hdr.txt

# Plot (generates HTML graph)
vegeta attack -rate=100 -duration=30s -targets=targets.txt | \
  vegeta plot > plot.html && open plot.html

# Save results for later analysis
vegeta attack -rate=100 -duration=30s -targets=targets.txt > results.bin
vegeta report < results.bin
vegeta plot < results.bin > plot.html

vegeta Ramp-Up with Pacer

# Linear ramp-up from 10 to 100 rps over 30 seconds
vegeta attack \
  -rate=10 \
  -duration=30s \
  -targets=targets.txt | vegeta report

# For ramp-up, generate a target file and use a script:
# vegeta doesn't natively support ramp-up; use k6 for complex scenarios

Analyzing Results

# Full text report
vegeta report < results.bin

# Example output:
# Requests      [total, rate, throughput] 3000, 100.00, 99.50/s
# Duration      [total, attack, wait]     30.005s, 30.000s, 4.567ms
# Latencies     [min, mean, 50, 90, 95, 99, max] 1.2ms, 4.5ms, 3.1ms, 8.2ms, 12.1ms, 45.6ms, 234ms
# Bytes In      [total, mean]             450000, 150.00
# Bytes Out     [total, mean]             0, 0.00
# Success       [ratio]                   99.80%
# Status Codes  [code:count]              200:2994 500:6
# Error Set:
# 500 Internal Server Error

# Parse JSON report for programmatic use
vegeta report -type=json < results.bin | jq '{
  requests: .requests,
  success_ratio: .success,
  latency_p95: .latencies.p95,
  latency_p99: .latencies.p99,
  status_codes: .status_codes
}'

CI/CD Integration

#!/bin/bash
# load-test.sh — run in CI after deployment
set -euo pipefail

BASE_URL="${BASE_URL:-http://localhost:8080}"
PASS_P95_MS="${PASS_P95_MS:-500}"

# Run k6 test
k6 run \
  --env BASE_URL="$BASE_URL" \
  --env API_TOKEN="$API_TOKEN" \
  --out json=results.json \
  load-test.js

# Check thresholds passed
if [ $? -ne 0 ]; then
  echo "Load test FAILED: thresholds not met"
  exit 1
fi

echo "Load test PASSED"
# GitHub Actions
- name: Run load tests
  run: |
    k6 run \
      --env BASE_URL=${{ secrets.STAGING_URL }} \
      --env API_TOKEN=${{ secrets.API_TOKEN }} \
      load-test.js
  if: github.ref == 'refs/heads/main'

Interpreting Results

Key Metrics to Monitor

Metric Target Warning Critical
p50 latency <100ms >200ms >500ms
p95 latency <500ms >1s >2s
p99 latency <1s >2s >5s
Error rate <0.1% >1% >5%
Throughput >target RPS <80% target <50% target
# Parse k6 JSON output for key metrics
jq -r '
  select(.type == "Point" and .metric == "http_req_duration") |
  .data.tags.percentile as $p |
  select($p == "p(95)") |
  "\(.data.time) p95=\(.data.value)ms"
' results.json | tail -20

# Check if thresholds were met
jq '.root_group.checks | to_entries[] | {name: .key, passes: .value.passes, fails: .value.fails}' results.json

Troubleshooting

# k6: increase file descriptor limit for high VU counts
ulimit -n 65535

# k6: debug HTTP requests
k6 run --http-debug=full script.js

# vegeta: verbose output
vegeta attack -rate=10 -duration=5s -targets=targets.txt 2>&1

# Common issues:
# Connection refused — ensure the target server is running
# Too many open files — increase ulimit before testing
# Results plateau — server is at capacity; this is expected behavior
# High latency variance — network or GC pauses; run from same region as target