Back to Blog
Load TestingPerformanceBenchmarkingScalability

Load Testing and Performance Benchmarking Guide

Validate your application can handle production traffic. From load testing tools to interpreting results to optimization strategies.

B
Bootspring Team
Engineering
February 18, 2025
6 min read

Load testing validates that your application handles expected (and unexpected) traffic levels. Without it, you're hoping your system works—with it, you know.

Types of Performance Testing#

Load Testing: - Expected traffic levels - Verify system meets requirements - Find breaking points Stress Testing: - Beyond normal capacity - Find system limits - Observe failure modes Spike Testing: - Sudden traffic bursts - Flash sale scenarios - Viral content handling Soak Testing: - Extended duration - Memory leaks - Resource exhaustion

Load Testing Tools#

1// k6 script: load-test.js 2import http from 'k6/http'; 3import { check, sleep } from 'k6'; 4 5export const options = { 6 stages: [ 7 { duration: '2m', target: 100 }, // Ramp up to 100 users 8 { duration: '5m', target: 100 }, // Stay at 100 users 9 { duration: '2m', target: 200 }, // Ramp up to 200 users 10 { duration: '5m', target: 200 }, // Stay at 200 users 11 { duration: '2m', target: 0 }, // Ramp down 12 ], 13 thresholds: { 14 http_req_duration: ['p(95)<500'], // 95% of requests under 500ms 15 http_req_failed: ['rate<0.01'], // Less than 1% errors 16 }, 17}; 18 19export default function () { 20 const res = http.get('https://api.example.com/products'); 21 22 check(res, { 23 'status is 200': (r) => r.status === 200, 24 'response time < 500ms': (r) => r.timings.duration < 500, 25 }); 26 27 sleep(1); 28}

Artillery#

1# artillery.yml 2config: 3 target: "https://api.example.com" 4 phases: 5 - duration: 120 6 arrivalRate: 10 7 name: "Warm up" 8 - duration: 300 9 arrivalRate: 50 10 name: "Sustained load" 11 - duration: 60 12 arrivalRate: 100 13 name: "Spike" 14 15scenarios: 16 - name: "Browse and purchase" 17 weight: 70 18 flow: 19 - get: 20 url: "/products" 21 - think: 2 22 - get: 23 url: "/products/{{ $randomNumber(1, 100) }}" 24 - think: 3 25 - post: 26 url: "/cart" 27 json: 28 productId: "{{ $randomNumber(1, 100) }}" 29 quantity: 1 30 31 - name: "Search" 32 weight: 30 33 flow: 34 - get: 35 url: "/search?q={{ $randomString(5) }}"

Autocannon (Node.js)#

1const autocannon = require('autocannon'); 2 3async function runBenchmark() { 4 const result = await autocannon({ 5 url: 'http://localhost:3000/api/users', 6 connections: 100, 7 duration: 30, 8 pipelining: 10, 9 headers: { 10 'Authorization': 'Bearer token', 11 }, 12 }); 13 14 console.log(autocannon.printResult(result)); 15} 16 17runBenchmark();

Realistic Load Patterns#

User Journey Simulation#

1// k6: Simulate real user behavior 2import http from 'k6/http'; 3import { group, sleep } from 'k6'; 4 5export default function () { 6 group('Homepage', () => { 7 http.get('https://example.com/'); 8 sleep(2 + Math.random() * 3); // 2-5 seconds think time 9 }); 10 11 group('Browse products', () => { 12 http.get('https://example.com/products'); 13 sleep(1 + Math.random() * 2); 14 15 http.get('https://example.com/products/123'); 16 sleep(3 + Math.random() * 5); 17 }); 18 19 group('Add to cart', () => { 20 http.post('https://example.com/cart', JSON.stringify({ 21 productId: '123', 22 quantity: 1, 23 }), { 24 headers: { 'Content-Type': 'application/json' }, 25 }); 26 sleep(1); 27 }); 28 29 group('Checkout', () => { 30 http.get('https://example.com/checkout'); 31 sleep(5 + Math.random() * 10); // Filling form 32 33 http.post('https://example.com/orders', JSON.stringify({ 34 paymentMethod: 'card', 35 }), { 36 headers: { 'Content-Type': 'application/json' }, 37 }); 38 }); 39}

Traffic Distribution#

1// Different endpoints have different traffic 2export const options = { 3 scenarios: { 4 browse: { 5 executor: 'constant-vus', 6 vus: 80, 7 duration: '10m', 8 exec: 'browseScenario', 9 }, 10 purchase: { 11 executor: 'constant-vus', 12 vus: 15, 13 duration: '10m', 14 exec: 'purchaseScenario', 15 }, 16 admin: { 17 executor: 'constant-vus', 18 vus: 5, 19 duration: '10m', 20 exec: 'adminScenario', 21 }, 22 }, 23};

Measuring Results#

Key Metrics#

Response Time: - Average: Overall performance - p50 (median): Typical user experience - p95: Most users' experience - p99: Worst case for most - Max: Absolute worst case Throughput: - Requests per second (RPS) - Transactions per second (TPS) - Data transferred per second Errors: - Error rate percentage - Error types distribution - Errors under load vs normal Resources: - CPU utilization - Memory usage - Network I/O - Database connections

Setting Targets#

1// Define acceptable thresholds 2export const options = { 3 thresholds: { 4 // Response time targets 5 http_req_duration: [ 6 'p(50)<200', // Median under 200ms 7 'p(95)<500', // 95th percentile under 500ms 8 'p(99)<1000', // 99th percentile under 1s 9 ], 10 11 // Error rate targets 12 http_req_failed: ['rate<0.01'], // <1% errors 13 14 // Throughput targets 15 http_reqs: ['rate>100'], // >100 requests/second 16 17 // Custom metrics 18 'my_custom_metric': ['avg<100'], 19 }, 20};

Database Load Testing#

1// Test database queries under load 2import sql from 'k6/x/sql'; 3 4const db = sql.open('postgres', 'postgres://user:pass@localhost/db'); 5 6export default function () { 7 // Read-heavy scenario 8 const results = sql.query(db, ` 9 SELECT * FROM orders 10 WHERE created_at > NOW() - INTERVAL '7 days' 11 ORDER BY created_at DESC 12 LIMIT 100 13 `); 14 15 // Write scenario 16 sql.query(db, ` 17 INSERT INTO events (user_id, event_type, created_at) 18 VALUES ($1, $2, NOW()) 19 `, [Math.floor(Math.random() * 10000), 'page_view']); 20}

CI/CD Integration#

1# .github/workflows/load-test.yml 2name: Load Test 3 4on: 5 push: 6 branches: [main] 7 schedule: 8 - cron: '0 2 * * *' # Daily at 2 AM 9 10jobs: 11 load-test: 12 runs-on: ubuntu-latest 13 steps: 14 - uses: actions/checkout@v3 15 16 - name: Setup k6 17 uses: grafana/setup-k6-action@v1 18 19 - name: Run load test 20 run: k6 run --out json=results.json load-test.js 21 22 - name: Check thresholds 23 run: | 24 if grep -q '"thresholds":{"http_req_duration":\[{"ok":false' results.json; then 25 echo "Performance thresholds not met" 26 exit 1 27 fi 28 29 - name: Upload results 30 uses: actions/upload-artifact@v3 31 with: 32 name: load-test-results 33 path: results.json

Analyzing Results#

Finding Bottlenecks#

Symptoms → Likely Causes High CPU: - Inefficient algorithms - Missing caching - Synchronous operations High Memory: - Memory leaks - Large object retention - Missing cleanup High Latency: - Database queries - External API calls - Network issues High Error Rate: - Resource exhaustion - Timeout misconfigurations - Connection pool limits

Performance Profiling#

1// Add profiling to identify slow code 2const { performance } = require('perf_hooks'); 3 4async function profiledEndpoint(req, res) { 5 const marks = {}; 6 7 marks.start = performance.now(); 8 9 // Database query 10 marks.dbStart = performance.now(); 11 const data = await db.query('SELECT * FROM users'); 12 marks.dbEnd = performance.now(); 13 14 // Processing 15 marks.processStart = performance.now(); 16 const processed = processData(data); 17 marks.processEnd = performance.now(); 18 19 // Response 20 marks.end = performance.now(); 21 22 // Log timings 23 console.log({ 24 total: marks.end - marks.start, 25 database: marks.dbEnd - marks.dbStart, 26 processing: marks.processEnd - marks.processStart, 27 }); 28 29 res.json(processed); 30}

Optimization Strategies#

Quick Wins#

1// 1. Add caching 2const cache = new Map(); 3 4async function getCachedData(key: string): Promise<Data> { 5 if (cache.has(key)) return cache.get(key); 6 const data = await fetchData(key); 7 cache.set(key, data); 8 return data; 9} 10 11// 2. Connection pooling 12const pool = new Pool({ 13 max: 20, 14 idleTimeoutMillis: 30000, 15 connectionTimeoutMillis: 2000, 16}); 17 18// 3. Batch operations 19async function getUsersOptimized(ids: string[]): Promise<User[]> { 20 // Instead of N queries, do 1 21 return db.users.findMany({ where: { id: { in: ids } } }); 22} 23 24// 4. Async where possible 25app.post('/order', async (req, res) => { 26 const order = await createOrder(req.body); 27 28 // Don't wait for these 29 sendConfirmationEmail(order).catch(console.error); 30 updateAnalytics(order).catch(console.error); 31 32 res.json(order); 33});

Conclusion#

Load testing is essential for production confidence. Test regularly, set realistic thresholds, and integrate into CI/CD. The goal isn't just finding limits—it's understanding your system's behavior under stress.

Remember: test in an environment as close to production as possible. Load test results are only as good as the environment they're run in.

Share this article

Help spread the word about Bootspring