Why a Real Load Test Is Worth It Before Launch

Lots of teams treat "load testing" as a script you run the night before launch and skim the numbers. But the real value of a load test is exposing bottlenecks early: a 2-core, 4GB entry-level cloud server can look great at 50 concurrent users and then see latency jump tenfold at 300. Better to have a script find that out than your first real user.

This checklist splits load testing into four parts: set the bar, pick the tool, run realistic scenarios, and read the metrics.

Step 1: Write Down the Pass Bar First

A load test without acceptance criteria is no test at all. Put this threshold table into your acceptance doc before you start, rather than deciding afterward:

Metric Healthy Warning Fail
CPU peak < 60% 80% 90% sustained 5 min
Memory peak < 70% 85% 90%+ and climbing
p95 response time < 400ms 800ms 1200ms
Error rate (5xx/timeout) < 0.1% 0.5% 1%
Disk I/O no queueing occasional stalls persistent blocking

Note that "85% CPU / 90% memory" are not recommended operating levels — they're "go investigate now" alerts. The question a load test answers is "at what load does this machine start degrading," not "what's the maximum it can handle."

Step 2: Pick the Right Tool

There are three common tiers of load testing tools:

Tool Strength Best For Learning Curve
wrk Single-machine, C-based, very high throughput Quick API/Nginx stress Low
k6 Scriptable, scenario orchestration, threshold assertions Realistic user paths, CI Medium
Locust Python, distributed Complex flows, Python teams Medium

For personal sites and small projects, k6 is usually enough: you can script real user paths (login, browse, checkout) and set thresholds so a failing run exits non-zero. A minimal k6 script:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },
    { duration: '2m', target: 100 },
    { duration: '2m', target: 300 },
    { duration: '2m', target: 500 },
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    http_req_failed: ['rate<0.005'],
    http_req_duration: ['p(95)<800'],
  },
};

export default function () {
  const res = http.get('https://your-site.example/');
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(1);
}

This ramps concurrency from 50 to 500 in four stages, then back down to 0, with two guardrails: error rate below 0.5% and p95 latency below 800ms. Cross a threshold mid-run and k6 fails the run.

Step 3: Load Test Real Traffic, Not Just the Homepage

Pounding only the homepage is the most common shortcut. Real traffic is mixed: static assets come from the CDN, product lists hit cache, and checkout hits the database directly. Cover at least three request classes:

  • Static assets (images/CSS/JS): validates CDN origin fetch and bandwidth;
  • Read endpoints (lists, detail pages): validates queries and cache hits;
  • Write endpoints (checkout, form submits): validates transactions, locks, disk I/O.

When load testing write endpoints, watch the slow-query log and the connection pool. A lot of "failed" load tests aren't the machine's fault — the database connection count maxes out and every request queues waiting for a connection.

Step 4: Locate the Bottleneck with Metrics

Run the test with monitoring on. Here's a symptom → bottleneck reference:

Symptom Likely Bottleneck Where to Look
Slow responses, high CPU App logic or DB queries top, slow-query log
Memory climbing, never drops Memory leak Process RSS, GC logs
Disk I/O stalled Slow-query write amplification, log flushing iostat, iotop
Bandwidth maxes out first Static assets bypassing the CDN iftop, CDN origin stats
Jittery latency, random timeouts Connection pool exhaustion, GC pauses Pool metrics, GC curves

One rule while debugging: change one variable at a time. If you enable Nginx caching, add DB indexes, and switch PHP to OPcache all at once, you won't know which one actually helped.

A Real Scenario: A Flash-Sale Traffic Spike

Say you run an e-commerce mini-program for users in China. On a normal day peak concurrency is around 200, but on the annual flash-sale evening it jumps to 1,200. Don't load test at "normal 200 concurrency" — replay the actual event curve: fast climb in the first five minutes, 30 minutes at peak, then a slow tail-off.

This kind of test usually exposes two things: first, the database connection pool is sized for everyday traffic, so at peak the connections get exhausted and read endpoints start timing out; second, static assets aren't served from a separate CDN hostname, so image requests saturate the outbound bandwidth and drag down API latency. Both show up clearly in the report — long before users screenshot their complaints.

What a Load Test Should Deliver

A load test isn't just about "passing" — it should leave evidence. A decent report includes:

  1. Environment: machine specs, software versions, tool and parameters;
  2. Staged results: QPS, p95/p99 latency, and error rate per concurrency tier;
  3. Bottleneck list: ranked findings with evidence (screenshots, slow queries);
  4. Recommended actions: each tied to a concrete change, not "optimize performance."

Reference: k6 docs https://grafana.com/docs/k6/latest/ , wrk repo https://github.com/wg/wrk

Common Mistakes

A few things I regularly see go wrong: testing locally instead of against a production-like environment (local networking and caching are completely different); looking only at average latency instead of p95 (averages hide behind a few fast requests); skipping the slow-query log after the run (most latency jumps are database-caused); and running the load generator on the same box as the target server (they interfere and skew the results).