NodeJSLoad Testing

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?

Stress finds the breaking point; soak finds slow leaks — they're not the same test
These aren't interchangeable. A **load test** validates normal and peak conditions; a **stress test** deliberately pushes past capacity to learn *how* the system fails — does latency degrade gracefully, or does it collapse and stop responding? A **spike test** simulates a sudden surge to check autoscaling and resilience. A **soak test** runs moderate load for *hours* — the only way to catch slow [memory leaks](/nodejs/memory-leaks), connection-pool exhaustion, and gradual degradation that a five-minute test never reveals. A complete picture needs several of these, not just one short burst.
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.

Watch the latency *tail* (p99), not the average — and find the load where it spikes
Throughput alone is misleading. A server can report "10,000 req/s" while the slowest 1% of those requests take 8 seconds — a terrible experience for thousands of users. Track **percentiles** and especially the tail (p99/p99.9), because that tail is where real users hit timeouts. The most valuable output of a load test is the **saturation point**: the concurrency level at which latency suddenly climbs and throughput stops rising. That knee in the curve is your real capacity — run *below* it in production and scale out before you reach it. Push past it deliberately in a stress test to learn your failure mode.
autocannon — fast Node-native benchmarking

Bash
# 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
`autocannon` is the quickest way to benchmark a single endpoint and read latency percentiles
`autocannon` is a Node-native HTTP benchmarking tool (`npx autocannon ...`) that fires concurrent requests and reports throughput and a full latency percentile breakdown in seconds — ideal for quick before/after comparisons when you're optimizing a specific endpoint. The `-c` flag sets concurrent connections, `-d` the duration. It pairs perfectly with a [profiler](/nodejs/profiling) or `0x`: run `autocannon` to generate steady load while you capture a flame graph of the server under that load, so the profile reflects realistic conditions rather than an idle process.
k6 and Artillery — realistic scenarios

k6 — scripted load test with stages and thresholds

JS
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
}
k6 lets you script user journeys and set pass/fail thresholds — ideal for CI performance gates
Single-endpoint hammering (autocannon) measures one route; **k6** and **Artillery** model *realistic user behaviour* — multi-step journeys (log in → browse → add to cart → checkout), ramp-up/steady/ramp-down stages, and per-user think-time. k6's `thresholds` turn a load test into a **pass/fail gate** you can run in CI: if p99 latency or error rate crosses a limit, the test fails and the deploy is blocked — the performance equivalent of a failing unit test. This catches performance regressions before they ship, not after users complain.
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.

Localhost results lie — test a production-like setup, and never co-locate the load generator with the server
Two mistakes invalidate most load tests. First, **testing on localhost or a dev box**: with no network latency, a tiny dataset, and a single instance, the numbers bear no relation to production — a query that's instant against 100 rows crawls against 10 million. Test against a staging environment that mirrors production sizing, database, and data volume. Second, **running the load generator on the same machine as the server**: the two fight for CPU, so you measure their contention rather than the server's true capacity. Run the generator from a separate host (ideally across a real network) and monitor the *server's* resources during the run to identify the actual bottleneck.
Next
Pull it all together into a checklist: [Optimization Best Practices](/nodejs/optimization-tips).