Hermes Wiki
Developer/DeveloperTools/Testing/LoadPerformance/Fundamentals/load-testing-methodology-and-tooling

Load Testing Methodology and Tooling

Concept

Load testing answers a question functional testing can't: not "does this endpoint return the right response," but "does the system still return the right response, within an acceptable latency, when many requests hit it at once." A load test generates synthetic traffic against a system — ramping request volume up over time, holding it steady, or spiking it suddenly — while measuring throughput (requests/second the system actually sustains, as opposed to what was offered), latency distribution (not just average latency, which hides the tail — p95/p99 matter more for user experience), and error rate under that load. The result tells a team where the system's actual capacity ceiling is, and which component degrades first as load increases (a specific endpoint, a database connection pool, a downstream dependency).

Modern load-testing tools fall into two camps by scripting model. k6 (Go-based core, JavaScript/TypeScript test scripts) and Artillery (YAML config with JavaScript hooks) run virtual users as lightweight, non-blocking event loops, which lets a single machine generate meaningfully high load without the overhead of one OS thread or process per virtual user. Locust (pure Python) instead models each virtual user as normal, readable Python code, which is easier to write complex, stateful user-journey scripts in, but scales load generation less efficiently per machine because of Python's own runtime overhead (and GIL, without going distributed) — Locust compensates for this by supporting distributed load generation (many worker processes/machines coordinated by one master) out of the box.

Tradeoffs

Tool Scripting model Single-machine throughput Best fit
k6 JavaScript/TypeScript, Go runtime High — low per-VU overhead, single static binary Teams already in the JS/TS ecosystem; CI-embedded performance testing; modern protocol support (HTTP, WebSocket, gRPC)
Locust Python Lower per-machine, but scales horizontally via built-in distributed workers Python-heavy teams; complex, highly stateful user-journey scripts that read more naturally as imperative Python than as declarative config
Artillery YAML + JavaScript hooks High — similar Node-based event-loop model to k6 Teams wanting the lowest-friction start (declarative YAML for simple scenarios); built-in browser-level and WebSocket testing support

None of these numbers matter as much as whether the load generator itself becomes the bottleneck — a load test that reports throughput being capped by the generator's CPU or network stack rather than the system under test is measuring the wrong thing, and every tool above has to be run distributed across multiple machines once traffic targets exceed one host's generation capacity.

When to use / when not to

  • Load-test before a known traffic event — a launch, a marketing push, a seasonal spike — where the cost of finding a capacity ceiling in production during the event is much higher than finding it in a controlled test beforehand.
  • Target the specific endpoint or workflow most likely to be the bottleneck first (typically the read-heavy, most-trafficked path, or the one hitting the most expensive downstream dependency), rather than uniformly hammering the whole surface area — a full-system test is more expensive to build and interpret, and usually just confirms what a targeted test already found.
  • Run load tests against an environment that's representative of production scale (data volume, instance sizing, network topology) — a load test against a downsized staging environment measures that environment's ceiling, not production's, and the two don't reliably scale linearly.
  • Skip formal load testing at early MVP stage before there's real traffic data to calibrate expected load against — a load test built on guessed traffic shapes tests a hypothesis, not a requirement, and the effort is better spent once actual usage patterns exist to model realistically.
  • Don't treat a single load test run as a permanent capacity number — code changes, dependency version bumps, and data growth all shift where the ceiling actually is; load testing is a practice to repeat, not a one-time certification.

Common pitfall

Testing with unrealistic traffic shape — a flat, uniform ramp that doesn't resemble how real users actually arrive (bursty, correlated with external events, uneven across endpoints) — and then treating the resulting capacity number as if it applies to real-world load patterns. A system that handles 1,000 evenly-spaced requests/second comfortably can still fall over under a genuine spike of the same average rate, because queueing and resource contention behave very differently under bursty arrival than under smooth arrival. The fix is modeling the test's ramp-up, hold, and (critically) spike/soak phases after actual or realistically-projected traffic patterns, not just picking a target RPS and holding it flat.

A second, related pitfall: reporting only average latency. Average latency can look perfectly healthy while a meaningful fraction of real users experience multi-second waits, because a small number of slow outliers get averaged away by a much larger number of fast requests. p95/p99 latency (and, for genuinely critical paths, p99.9) is what actually reflects the tail-end user experience a capacity decision should be made against.

Engineering Lens

The load-testing tool choice (k6 vs. Locust vs. Artillery) is a secondary decision — the primary one is whether the test's traffic shape and target environment are representative enough that its result generalizes to the real failure mode being guarded against. A beautifully-scripted load test against an unrepresentative environment, using a traffic shape nothing like production, produces a confidently wrong capacity number — which is worse than no number at all, because it gets trusted. The Principal-level question in a capacity review isn't "did we load test it" but "does this test's traffic shape and target environment actually resemble the event we're trying to survive."

Sources

Hermes Wiki