Performance Testing
Load testing, stress testing, and performance benchmarking strategies
Performance testing ensures your application can handle expected (and unexpected) load while maintaining acceptable response times. For regulated systems, performance is often a compliance requirement affecting patient safety or data integrity.
Types of Performance Testing
| Type | Purpose | When to Use |
|---|---|---|
| Load Testing | Test expected load | Before release, after major changes |
| Stress Testing | Find breaking points | Capacity planning |
| Spike Testing | Handle sudden load increases | Traffic surge scenarios |
| Endurance Testing | Long-term stability | Memory leaks, resource exhaustion |
| Scalability Testing | Scaling behavior | Cloud infrastructure planning |
Performance Requirements
Defining SLAs and SLOs
| Metric | Definition | Typical Target |
|---|---|---|
| Response Time (P50) | Median response time | < 200ms |
| Response Time (P95) | 95th percentile | < 500ms |
| Response Time (P99) | 99th percentile | < 1000ms |
| Throughput | Requests per second | Varies by endpoint |
| Error Rate | Failed requests | < 0.1% |
| Availability | Uptime percentage | 99.9% |
For Regulated Systems
SLA Example for Healthcare System:
- Patient lookup API: P95 < 500ms
- Critical alerts: P99 < 100ms
- Report generation: < 30s for standard reports
- Concurrent users: Support 500 simultaneous users
- Availability: 99.99% during business hoursLoad Testing with k6
Basic Load Test
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', 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%
},
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}Testing API Endpoints
import http from 'k6/http';
import { check, group } from 'k6';
const BASE_URL = 'https://api.example.com';
export default function () {
// Login
group('Authentication', function () {
const loginRes = http.post(`${BASE_URL}/auth/login`, {
email: 'user@example.com',
password: 'password123',
});
check(loginRes, {
'login successful': (r) => r.status === 200,
'has token': (r) => r.json('token') !== undefined,
});
const token = loginRes.json('token');
// Use token for subsequent requests
const headers = { Authorization: `Bearer ${token}` };
// Get user profile
group('User Profile', function () {
const profileRes = http.get(`${BASE_URL}/profile`, { headers });
check(profileRes, {
'profile loaded': (r) => r.status === 200,
'has user data': (r) => r.json('id') !== undefined,
});
});
// List resources
group('List Resources', function () {
const resourcesRes = http.get(`${BASE_URL}/resources`, { headers });
check(resourcesRes, {
'resources loaded': (r) => r.status === 200,
'is array': (r) => Array.isArray(r.json()),
});
});
});
}Stress Testing
Finding Breaking Points
// stress-test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Below normal load
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 }, // Normal load
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 }, // Around breaking point
{ duration: '5m', target: 300 },
{ duration: '2m', target: 400 }, // Beyond breaking point
{ duration: '5m', target: 400 },
{ duration: '10m', target: 0 }, // Recovery
],
};
export default function () {
const res = http.get('https://api.example.com/health');
check(res, {
'is healthy': (r) => r.status === 200,
});
}Spike Testing
// spike-test.js
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Normal load
{ duration: '10s', target: 500 }, // Spike!
{ duration: '3m', target: 500 }, // Stay at spike
{ duration: '10s', target: 50 }, // Recovery
{ duration: '3m', target: 50 },
{ duration: '10s', target: 500 }, // Another spike
{ duration: '3m', target: 500 },
{ duration: '1m', target: 0 },
],
};Database Performance Testing
Query Performance
import sql from 'k6/x/sql';
const db = sql.open('postgres', 'postgres://user:pass@localhost:5432/test');
export function setup() {
// Seed test data if needed
}
export default function () {
// Test simple query
const results = sql.query(db, 'SELECT * FROM users WHERE id = $1', [1]);
check(results, {
'query returns data': (r) => r.length > 0,
});
}
export function teardown() {
db.close();
}Connection Pool Testing
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
// Simulate connection exhaustion
high_concurrency: {
executor: 'constant-vus',
vus: 100, // More than pool size
duration: '5m',
},
},
};
export default function () {
// Each request acquires a DB connection
const res = http.get('https://api.example.com/users');
check(res, {
'no connection errors': (r) => r.status !== 503,
});
}API Performance Testing
Benchmarking Endpoints
// benchmark.js
import http from 'k6/http';
import { Trend } from 'k6/metrics';
const getUserTrend = new Trend('get_user_duration');
const listUsersTrend = new Trend('list_users_duration');
const createUserTrend = new Trend('create_user_duration');
export const options = {
vus: 10,
duration: '5m',
};
export default function () {
// GET /users/:id
let res = http.get('https://api.example.com/users/1');
getUserTrend.add(res.timings.duration);
// GET /users
res = http.get('https://api.example.com/users');
listUsersTrend.add(res.timings.duration);
// POST /users
res = http.post('https://api.example.com/users', JSON.stringify({
name: 'Test User',
email: `test${Date.now()}@example.com`,
}), {
headers: { 'Content-Type': 'application/json' },
});
createUserTrend.add(res.timings.duration);
}Frontend Performance Testing
Lighthouse CI
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/', 'http://localhost:3000/dashboard'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 300 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};Web Vitals Testing
import { test, expect } from '@playwright/test';
test('should meet Web Vitals thresholds', async ({ page }) => {
await page.goto('/');
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
const results = {};
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
results[entry.name] = entry.value;
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
results.cls = entry.value;
}
}).observe({ type: 'layout-shift', buffered: true });
setTimeout(() => resolve(results), 5000);
});
});
expect(metrics.LCP).toBeLessThan(2500);
expect(metrics.cls).toBeLessThan(0.1);
});Performance Testing in CI/CD
GitHub Actions Integration
name: Performance Tests
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # Nightly
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start application
run: docker-compose up -d
- name: Wait for application
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Run k6 load test
uses: grafana/k6-action@v0.3.0
with:
filename: tests/performance/load-test.js
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v10
with:
configPath: ./lighthouserc.js
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: performance-results
path: |
summary.json
lighthouse-report.htmlPerformance Baseline and Regression
Establishing Baselines
// baseline-test.js
import http from 'k6/http';
import { check } from 'k6';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js';
export const options = {
vus: 50,
duration: '10m',
thresholds: {
'http_req_duration{endpoint:get_users}': ['p(95)<200'],
'http_req_duration{endpoint:get_user}': ['p(95)<100'],
'http_req_duration{endpoint:create_user}': ['p(95)<500'],
},
};
export default function () {
// Tag requests for separate metrics
http.get('https://api.example.com/users', {
tags: { endpoint: 'get_users' },
});
http.get('https://api.example.com/users/1', {
tags: { endpoint: 'get_user' },
});
}
export function handleSummary(data) {
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
'summary.json': JSON.stringify(data),
};
}Comparing Against Baseline
// Compare current run against baseline
import { previousRun } from './baseline.json';
export const options = {
thresholds: {
// Fail if more than 10% slower than baseline
http_req_duration: [
`p(95)<${previousRun.p95 * 1.1}`,
],
},
};Performance Testing Checklist
Before Testing
- Define performance requirements (SLAs/SLOs)
- Identify critical endpoints and workflows
- Set up monitoring and metrics collection
- Prepare test data and environment
During Testing
- Monitor system resources (CPU, memory, network)
- Watch for error rates and timeouts
- Capture detailed metrics and logs
- Test at multiple load levels
After Testing
- Analyze results against requirements
- Identify bottlenecks and optimization opportunities
- Document findings and recommendations
- Update baselines if acceptable
Related Resources
- Unit Testing
- Test Automation
- Observability
- k6 Documentation - Modern load testing tool
- Grafana k6 - Official k6 documentation
Compliance
This section fulfills ISO 13485 requirements for process validation (7.5.6) and monitoring and measurement (8.2.4), and ISO 27001 requirements for capacity management (A.8.6) and secure development lifecycle (A.8.25).
How is this guide?
Last updated on