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#
k6 (Recommended)#
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.jsonAnalyzing 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.