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
- Baseline Testing: Establish performance under normal conditions
- Load Testing: Test under expected peak traffic
- Stress Testing: Push beyond normal capacity
- Spike Testing: Test sudden traffic increases
- 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
- Normal Operation: 80% of peak capacity
- Peak Traffic: 100% of expected peak
- Stress Conditions: 150% of peak capacity
- 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.