Load Testing
Load testing sends controlled, high-volume traffic at your application to answer questions you can't answer by eyeballing it: How many requests per second can it handle? What's the latency at p99 under load? Where does it fall over, and how does it fail? Without it, you discover your limits during a real traffic spike — the worst possible time. This page covers the types of load test, the metrics that matter, the Node-friendly tools (autocannon, k6, Artillery), how to design a realistic test, and how to read the results to find your true bottleneck.
Types of load test — different questions
Type | Question it answers |
|---|---|
Load test | How does it behave at expected/peak traffic? |
Stress test | Where does it break, and how (graceful vs catastrophic)? |
Spike test | Can it survive a sudden surge (a launch, a sale)? |
Soak test | Does it degrade over hours? (Reveals memory leaks, resource exhaustion) |
Capacity test | How many instances do I need for target traffic? |
The metrics that matter
Throughput — requests/second the system sustains. The headline capacity number.
Latency percentiles — p50, p95, p99 (and p99.9). The tail is what users feel; ignore averages.
Error rate — % of requests that fail or time out as load climbs.
Saturation point — the load level where latency spikes or throughput plateaus/drops.
Resource usage — CPU, memory, event-loop lag, DB connections during the run.
Concurrency — how many simultaneous connections/virtual users the test applied.
autocannon — fast Node-native benchmarking
# 100 concurrent connections for 30 seconds:
npx autocannon -c 100 -d 30 http://localhost:3000/api/products
# POST with a body and headers:
npx autocannon -c 50 -d 20 -m POST \
-H 'Content-Type=application/json' \
-b '{"email":"a@x.com"}' http://localhost:3000/api/users┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤ │ Latency │ 4 ms │ 9 ms │ 38 ms │ 71ms │ 11.2 ms │ 9.8 ms │ 210ms │ └─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘ Req/Sec: 8,640 avg | Bytes/Sec: 4.1 MB | 2xx: 259,200 non-2xx: 0
k6 and Artillery — realistic scenarios
k6 — scripted load test with stages and thresholds
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
stages: [
{ duration: '1m', target: 100 }, // ramp up to 100 virtual users
{ duration: '3m', target: 100 }, // hold (steady-state load)
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(99)<500'], // FAIL the test if p99 exceeds 500ms
http_req_failed: ['rate<0.01'], // FAIL if error rate exceeds 1%
},
}
export default function () {
const res = http.get('https://staging.example.com/api/products')
check(res, { 'status is 200': (r) => r.status === 200 })
sleep(1) // model think-time between user actions
}Designing a test that tells the truth
Test against a production-like environment — same instance sizes, real database, realistic data volume. Localhost numbers are fiction.
Don't load-test from the same machine as the server — the load generator competes for CPU and skews results.
Use realistic data & traffic mix — vary inputs, hit a representative spread of endpoints, model real ratios of reads to writes.
Include think-time — real users pause between actions; back-to-back requests overstate concurrency.
Warm up first — let JIT, caches, and connection pools stabilize before measuring.
Monitor the server during the run — capture CPU, memory, event-loop lag, and DB metrics so you can see what saturated.