Load Testing

Comprehensive load testing strategies to validate performance under various traffic conditions and identify bottlenecks.

Load Testing

Validate your application's performance under various load conditions and identify bottlenecks before they impact users.

Load Testing Strategy

Testing Phases

  1. Baseline Testing: Establish performance under normal conditions
  2. Load Testing: Test under expected peak traffic
  3. Stress Testing: Push beyond normal capacity
  4. Spike Testing: Test sudden traffic increases
  5. Volume Testing: Test with large amounts of data

Key Metrics to Monitor

  • Response Time: Average, median, 95th percentile
  • Throughput: Requests per second
  • Error Rate: Percentage of failed requests
  • Resource Utilization: CPU, memory, disk I/O
  • Database Performance: Query execution times

Load Testing Tools

Artillery Setup

Configure Artillery for comprehensive load testing:

# artillery-config.yml
config:
  target: 'https://your-app.com'
  phases:
    - duration: 60
      arrivalRate: 5
      name: "Warm up"
    - duration: 300
      arrivalRate: 50
      name: "Sustained load"
    - duration: 120
      arrivalRate: 100
      name: "Peak load"
  payload:
    path: "./test-data.csv"
    fields:
      - "productId"
      - "userId"

scenarios:
  - name: "Product browsing flow"
    weight: 60
    flow:
      - get:
          url: "/api/products/{{ productId }}"
          capture:
            - json: "$.id"
              as: "id"
      - get:
          url: "/api/inventory/{{ id }}"
      - think: 3
      
  - name: "Search and filter"
    weight: 30
    flow:
      - get:
          url: "/api/products/search?q=barcode"
      - get:
          url: "/api/products?category=electronics"
      - think: 2
      
  - name: "Dashboard metrics"
    weight: 10
    flow:
      - get:
          url: "/api/dashboard/metrics"
          headers:
            Authorization: "Bearer {{ $randomString() }}"

K6 Testing Scripts

Advanced load testing with k6:

// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const errorRate = new Rate('errors');

export let options = {
  stages: [
    { duration: '5m', target: 100 }, // Ramp up
    { duration: '10m', target: 100 }, // Stay at 100 users
    { duration: '5m', target: 200 }, // Ramp to 200 users
    { duration: '10m', target: 200 }, // Stay at 200 users
    { duration: '5m', target: 0 }, // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'], // Error rate under 1%
    errors: ['rate<0.01'],
  },
};

export default function() {
  // Test product listing
  let response = http.get('https://your-app.com/api/products');
  let success = check(response, {
    'Product listing status is 200': (r) => r.status === 200,
    'Product listing response time < 500ms': (r) => r.timings.duration < 500,
  });
  
  errorRate.add(!success);
  
  // Test product details
  if (response.json() && response.json().length > 0) {
    const productId = response.json()[0].id;
    response = http.get(`https://your-app.com/api/products/${productId}`);
    
    success = check(response, {
      'Product details status is 200': (r) => r.status === 200,
      'Product details response time < 300ms': (r) => r.timings.duration < 300,
    });
    
    errorRate.add(!success);
  }
  
  sleep(1);
}

Database Load Testing

Connection Pool Testing

Test database connection limits:

// test/load/db-connections.ts
import { Pool } from 'pg'

async function testDatabaseConnections() {
  const pools = []
  let activeConnections = 0
  
  try {
    // Create multiple connection pools
    for (let i = 0; i < 50; i++) {
      const pool = new Pool({
        connectionString: process.env.DATABASE_URL,
        max: 20,
      })
      
      pools.push(pool)
      
      // Test connection
      const client = await pool.connect()
      activeConnections++
      
      // Simulate work
      await client.query('SELECT NOW()')
      client.release()
    }
    
    console.log(`Successfully created ${activeConnections} connections`)
  } catch (error) {
    console.error(`Failed at ${activeConnections} connections:`, error)
  } finally {
    // Cleanup
    for (const pool of pools) {
      await pool.end()
    }
  }
}

Query Performance Testing

Test individual query performance:

// test/load/query-performance.ts
export async function testQueryPerformance() {
  const queries = [
    'SELECT * FROM products WHERE category = $1',
    'SELECT COUNT(*) FROM inventory WHERE quantity < 10',
    'SELECT p.*, i.quantity FROM products p JOIN inventory i ON p.id = i.product_id',
  ]
  
  for (const query of queries) {
    const startTime = Date.now()
    const iterations = 1000
    
    for (let i = 0; i < iterations; i++) {
      await db.query(query, ['electronics'])
    }
    
    const avgTime = (Date.now() - startTime) / iterations
    console.log(`Query: ${query.substring(0, 50)}... - Avg: ${avgTime}ms`)
  }
}

Performance Monitoring During Tests

Real-time Monitoring Setup

Monitor system resources during load tests:

# System monitoring script
#!/bin/bash
echo "Timestamp,CPU%,Memory%,DiskIO,NetworkIO" > performance-log.csv

while true; do
  timestamp=$(date "+%Y-%m-%d %H:%M:%S")
  cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
  memory=$(free | grep Mem | awk '{printf "%.2f", $3/$2 * 100.0}')
  disk_io=$(iostat -d 1 1 | grep -E '^[a-z]' | awk '{sum+=$4} END {print sum}')
  network_io=$(cat /proc/net/dev | grep eth0 | awk '{print $2+$10}')
  
  echo "$timestamp,$cpu,$memory,$disk_io,$network_io" >> performance-log.csv
  sleep 5
done

Application Metrics Collection

Collect application-specific metrics:

// lib/monitoring/load-test-metrics.ts
export class LoadTestMetrics {
  private static metrics = {
    requestCount: 0,
    errorCount: 0,
    responseTimes: [] as number[],
    activeConnections: 0,
  }
  
  static recordRequest(responseTime: number, isError: boolean = false) {
    this.metrics.requestCount++
    this.metrics.responseTimes.push(responseTime)
    
    if (isError) {
      this.metrics.errorCount++
    }
    
    // Keep only last 1000 response times to prevent memory issues
    if (this.metrics.responseTimes.length > 1000) {
      this.metrics.responseTimes = this.metrics.responseTimes.slice(-1000)
    }
  }
  
  static getMetrics() {
    const responseTimes = this.metrics.responseTimes.sort((a, b) => a - b)
    const count = responseTimes.length
    
    return {
      totalRequests: this.metrics.requestCount,
      errorRate: this.metrics.errorCount / this.metrics.requestCount,
      avgResponseTime: responseTimes.reduce((a, b) => a + b, 0) / count,
      p50: responseTimes[Math.floor(count * 0.5)],
      p95: responseTimes[Math.floor(count * 0.95)],
      p99: responseTimes[Math.floor(count * 0.99)],
    }
  }
}

Load Testing Best Practices

Test Environment

  • Use production-like data volumes
  • Test with realistic user flows
  • Include all system dependencies
  • Monitor all system layers

Test Scenarios

  1. Normal Operation: 80% of peak capacity
  2. Peak Traffic: 100% of expected peak
  3. Stress Conditions: 150% of peak capacity
  4. Breaking Point: Increase until failure

Continuous Load Testing

Integrate load testing into CI/CD:

# .github/workflows/load-test.yml
name: Load Test
on:
  schedule:
    - cron: '0 2 * * *' # Daily at 2 AM
  workflow_dispatch:

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run load tests
        run: |
          npm install -g artillery
          artillery run artillery-config.yml --output report.json
          
      - name: Generate report
        run: |
          artillery report report.json --output report.html
          
      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: load-test-report
          path: report.html

Regular load testing helps identify performance bottlenecks before they impact users and validates that your application can handle expected traffic volumes.