Master the five core performance testing types and learn to measure RPS, latency percentiles, error rate, and throughput to set data-driven performance baselines.
Performance bugs cost money and customers at a rate far exceeding functional bugs. Amazon quantified this in 2006: every 100 ms of latency cost them 1% of sales. Google research showed a 500 ms delay in search reduced traffic by 20%. A system that is functionally correct but responds in 8 seconds under load is not production-ready. Performance testing is the discipline of measuring, verifying, and improving system behaviour under realistic and extreme conditions before users encounter it.
This lesson introduces the five performance test types, defines the key metrics you will track in every test, and explains how to read the numbers that matter. By the end you will understand when to run each test type and how to interpret latency percentiles, error rates, and throughput figures.
Each test type answers a different business question. Running the wrong type produces misleading results.
Smoke Test (1-5 VUs, 1-2 min): Answers 'Is the system basically up and returning correct responses?' Run this before any destructive test to catch obvious failures fast.
Load Test (expected peak traffic, 15-30 min): Answers 'Can the system handle normal and peak load within SLOs?' This is the test you run most frequently — on every deployment.
Stress Test (2-5x expected peak, ramped): Answers 'Where does the system break and how does it recover?' Reveals bottlenecks (CPU, DB connection pool, memory).
Spike Test (10x normal in seconds): Answers 'How does the system handle flash sales or viral events?' Tests auto-scaling speed and circuit-breaker behaviour.
Soak/Endurance Test (normal load, 4-24 hours): Answers 'Does the system degrade over time?' Catches memory leaks, connection pool exhaustion, and disk fill-up.
1// k6 smoke test — verify baseline before destructive tests
2import http from 'k6/http';
3import { check } from 'k6';
4
5export const options = {
6 vus: 3,
7 duration: '1m',
8 thresholds: {
9 http_req_failed: ['rate<0.01'],
10 http_req_duration: ['p(95)<500'],
11 },
12};
13
14export default function () {
15 const res = http.get('https://api.example.com/health');
16 check(res, {
17 'status is 200': (r) => r.status === 200,
18 'response has ok': (r) => r.json('status') === 'ok',
19 });
20}Measuring performance without understanding the right metrics leads to false confidence. A mean latency of 200 ms looks great until you realize 5% of users experience 4 seconds.
Requests Per Second (RPS): How many requests the system completes per second. Measure both the offered load (what you send) and throughput (what the server completes). If they diverge, the system is saturating.
Latency Percentiles:
Always use percentiles in SLOs, never averages.
Error Rate: Percentage returning 5xx or timeout. Target <0.1% for production APIs.
1// k6 with explicit latency thresholds per endpoint
2import http from 'k6/http';
3import { check, sleep } from 'k6';
4
5export const options = {
6 stages: [
7 { duration: '5m', target: 100 },
8 { duration: '15m', target: 100 },
9 { duration: '5m', target: 0 },
10 ],
11 thresholds: {
12 'http_req_duration{name:list-products}': ['p(95)<300'],
13 'http_req_duration{name:create-order}': ['p(95)<800'],
14 'http_req_failed': ['rate<0.001'],
15 },
16};
17
18export default function () {
19 http.get('https://api.example.com/products',
20 { tags: { name: 'list-products' } });
21 sleep(0.5);
22 http.post('https://api.example.com/orders',
23 JSON.stringify({ cartId: 'c1' }),
24 { headers: { 'Content-Type': 'application/json' },
25 tags: { name: 'create-order' } });
26 sleep(1);
27}A baseline gives your numbers context. A p99 of 800 ms for a photo upload is excellent; for a login check it is unacceptable.
Establishing a Baseline: Run a smoke test against a production-equivalent environment. Record p50, p95, p99, error rate, and RPS at each VU count. New code must not regress beyond 10-20% on p95.
The Saturation Point: Plot RPS vs. VU count. Initially RPS grows linearly. Beyond saturation, adding VUs increases latency and errors but not RPS. This is your throughput ceiling.
1VUs: 1 10 50 100 200 300
2RPS: 5 48 215 390 395 390 saturation ~100 VUs
3p95ms: 80 85 120 450 2100 8000 latency explodes
4Errors: 0% 0% 0.1% 1.2% 12% 45%Reading Grafana Dashboards: A k6 + Grafana setup shows VUs over time (confirms ramp), request rate (server is processing), p95 latency (SLO line), and error rate (spike triggers investigation). Always correlate with server-side metrics (CPU, memory, DB connections) to identify the bottleneck layer.
Service Level Objectives translate business requirements into measurable thresholds. Without SLOs, 'performance testing passed' means nothing.
SLO Examples:
Error Budget: If your availability SLO is 99.9%, your error budget is 0.1% of requests — roughly 43.8 minutes of downtime per month. Performance testing helps you spend that budget wisely.
1// Structured SLO definition — translates directly to k6 thresholds
2const API_SLOS = {
3 endpoints: {
4 'GET /products': { p95Ms: 300, p99Ms: 800, errorRatePct: 0.1 },
5 'POST /orders': { p95Ms: 800, p99Ms: 2000, errorRatePct: 0.05 },
6 'GET /search': { p95Ms: 500, p99Ms: 1500, errorRatePct: 0.5 },
7 },
8 availability: 99.9,
9 sustainedRps: 500,
10} as const;
11
12// In k6:
13// thresholds: {
14// 'http_req_duration{name:GET /products}': ['p(95)<300'],
15// 'http_req_failed': ['rate<0.001'],
16// }Define SLOs before writing tests. Thresholds without SLOs are aspirations, not gates.
Choose the right tool for the right question:
k6 (Grafana Labs): JavaScript API for load testing, built-in metrics, Grafana integration, CI-friendly. Best for: API load testing, SLO gate enforcement in CI/CD pipelines. The tool covered in this module.
Lighthouse / WebPageTest: Measures frontend performance (LCP, CLS, INP, bundle sizes). Single-user scenarios only. Best for: Core Web Vitals auditing.
clinic.js / 0x: Node.js profiling. clinic flame generates flamegraphs; 0x identifies CPU hot paths. Best for: diagnosing why the server is slow after load tests reveal a problem.
EXPLAIN ANALYZE (PostgreSQL): Query execution plans. Best for: identifying missing indexes and N+1 query patterns.
1# Install k6
2brew install k6
3
4# Run with environment variable override
5k6 run --env BASE_URL=https://staging.example.com tests/perf/smoke.js
6
7# Export results for analysis
8k6 run --out json=results/run-$(date +%s).json tests/perf/load.js
9
10# Install clinic.js for Node.js profiling
11npm install -g clinic
12clinic flame -- node server.js
13# Then run k6 load test and observe flamegraphFor a Node.js/Next.js project: k6 for API load tests, Lighthouse CLI for Core Web Vitals, clinic.js for server-side profiling after load tests reveal a bottleneck.
A complete k6 smoke + load test covering the critical paths of a product catalog API with SLO-aligned thresholds and metric tagging for Grafana dashboards.
1// tests/perf/api-baseline.js
2import http from 'k6/http';
3import { check, group, sleep } from 'k6';
4import { Rate, Trend, Counter } from 'k6/metrics';
5
6const errorsByEndpoint = new Rate('errors_by_endpoint');
7const checkoutDuration = new Trend('checkout_duration_ms', true);
8const orderCreated = new Counter('orders_created_total');
9
10export const options = {
11 scenarios: {
12 smoke: {
13 executor: 'constant-vus',
14 vus: 3,
15 duration: '1m',
16 tags: { scenario: 'smoke' },
17 exec: 'smokeTest',
18 },
19 load: {
20 executor: 'ramping-vus',
21 startVUs: 0,
22 stages: [
23 { duration: '5m', target: 50 },
24 { duration: '15m', target: 50 },
25 { duration: '5m', target: 0 },
26 ],
27 tags: { scenario: 'load' },
28 exec: 'loadTest',
29 startTime: '2m',
30 },
31 },
32 thresholds: {
33 'http_req_duration': ['p(95)<500', 'p(99)<1500'],
34 'http_req_failed': ['rate<0.001'],
35 'http_req_duration{name:list-products}': ['p(95)<300'],
36 'http_req_duration{name:get-product}': ['p(95)<200'],
37 'http_req_duration{name:create-order}': ['p(95)<800'],
38 'checkout_duration_ms': ['p(95)<1000', 'p(99)<2000'],
39 },
40};
41
42const BASE_URL = __ENV.BASE_URL || 'https://api.example.com';
43
44export function smokeTest() {
45 group('Health check', () => {
46 const res = http.get(`${BASE_URL}/health`);
47 check(res, {
48 'health 200': (r) => r.status === 200,
49 'status ok': (r) => r.json('status') === 'ok',
50 });
51 });
52}
53
54export function loadTest() {
55 const headers = {
56 Authorization: 'Bearer test-token',
57 'Content-Type': 'application/json',
58 };
59
60 group('Product catalog', () => {
61 const listRes = http.get(
62 `${BASE_URL}/products?page=1&limit=20`,
63 { headers, tags: { name: 'list-products' } }
64 );
65 check(listRes, {
66 'list 200': (r) => r.status === 200,
67 'returns array': (r) => Array.isArray(r.json('data')),
68 });
69 errorsByEndpoint.add(listRes.status >= 400, { endpoint: 'list-products' });
70 sleep(0.5);
71
72 const productRes = http.get(
73 `${BASE_URL}/products/prod_123`,
74 { headers, tags: { name: 'get-product' } }
75 );
76 check(productRes, {
77 'detail 200': (r) => r.status === 200,
78 'has price': (r) => typeof r.json('price') === 'number',
79 });
80 });
81
82 sleep(1);
83
84 group('Checkout flow', () => {
85 const startTime = Date.now();
86 const cartRes = http.post(
87 `${BASE_URL}/cart`,
88 JSON.stringify({ productId: 'prod_123', quantity: 2 }),
89 { headers, tags: { name: 'add-to-cart' } }
90 );
91 check(cartRes, { 'cart 201': (r) => r.status === 201 });
92
93 if (cartRes.status === 201) {
94 const cartId = cartRes.json('cartId');
95 const orderRes = http.post(
96 `${BASE_URL}/orders`,
97 JSON.stringify({ cartId, paymentMethod: 'card_test' }),
98 { headers, tags: { name: 'create-order' } }
99 );
100 check(orderRes, { 'order 201': (r) => r.status === 201 });
101 if (orderRes.status === 201) {
102 orderCreated.add(1);
103 checkoutDuration.add(Date.now() - startTime);
104 }
105 }
106 });
107
108 sleep(1);
109}
110
111export function setup() {
112 console.log(`Starting performance test against ${BASE_URL}`);
113}This test uses k6 scenarios to run smoke and load tests sequentially. The smoke scenario (3 VUs, 1 minute) verifies API health before the load scenario ramps to 50 concurrent users over 25 minutes. Custom metrics (errorsByEndpoint, checkoutDuration) feed Grafana dashboards with endpoint-level visibility. Thresholds use tag filtering for per-endpoint SLO enforcement — p95 < 300 ms for product listing, p95 < 800 ms for order creation. Run: k6 run --env BASE_URL=https://staging-api.example.com tests/perf/api-baseline.js
The next lesson dives into writing your first k6 script — installing k6, structuring scripts with options, setup/teardown hooks, groups, checks, and reading the terminal summary output.
Your progress is saved in this browser, on this device only.