Module O-5·28 min read

Load vs soak vs spike tests, shadow and replay traffic, finding the knee of the curve, and a headroom target you can defend in a budget meeting.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module O-5 — Load Testing and Capacity Planning

What this module covers: Where every number in this course comes from — the rate limits, pool sizes, concurrency caps, shed thresholds and autoscaling targets that earlier modules kept describing as "derived from measurement." What a load test is actually looking for, which is the knee of the curve and the identity of the first resource to saturate, not a maximum requests-per-second figure. Five test types that find genuinely different bugs. Then the methodological error that invalidates most load tests: a closed-loop generator self-throttles under overload, so it cannot observe the failure you are testing for, and coordinated omission makes the latency it reports dramatically optimistic. Realistic key distributions and data volumes, because a test with a hot key measures a cache. Testing in production, and why only production has the real traffic mix. Then capacity planning: headroom derived from what you must survive, the arithmetic that turns "lose one of three availability zones" into a defensible utilisation target, autoscaling on the right signal, and the planning point that matters most — the lead time on your fix must be shorter than the time until you need it, which is why sharding is started before it is needed.


What You Are Actually Looking For

A load test's output is not a number to put in a slide. It is four findings:

  1. The knee of the curve — the offered load beyond which goodput degrades (Module A-8). Not the maximum throughput; the point where latency leaves its budget.
  2. Which resource saturates first, because that is the only thing worth scaling and everything else is wasted money.
  3. The failure mode at overload — do you shed cleanly with 503s, or do queues grow until everything times out? This is a test of Module A-8's mechanisms, and it is the finding teams most often skip.
  4. The numbers for rate limits, pool sizes, concurrency caps and autoscaling thresholds.

An untested capacity claim is a guess, and a launch is where guesses become incidents.

Analogy: a lift's weight limit. The useful number is not "the cables snapped at 2,400 kg" — nobody operates there. It is the load at which the lift starts behaving badly (slow, juddering, doors mistiming), which component reaches its limit first (the motor, not the cable), and what happens when someone exceeds the limit anyway: does it refuse to move and beep, or does it move and then stop between floors? A sign saying "8 persons" is the output; the test was about finding the knee and confirming the refusal works.


Five Test Types, Five Different Bug Classes

TestShapeFinds
Load / rampincrease until latency degradesthe knee; the binding resource
Soak / endurancemoderate load for hours or daysleaks, exhaustion, slow accumulation
Spikeinstant 10× then backadmission control, cold caches, autoscaler lag
Breakpointpush until it failshow it fails — shed or collapse
Recoverybreak it, then remove the loadwhether it comes back without intervention

Each finds things the others cannot:

Soak tests find the accumulation bugs, which are invisible in a twenty-minute run: a memory leak in a request-scoped cache; file descriptors or connections leaked on an error path; a log disk filling; an unbounded in-process Map used as a deduplicator (Module P-16); a bloat feedback loop where a high-churn table's dead tuples outpace autovacuum until queries slow enough to lengthen transactions enough to further block vacuum (Module F-13); a replication slot growing because a consumer is slightly slower than the write rate (Module P-23). Every one of those needs hours, not minutes.

Spike tests test the mechanisms of Module A-8 and expose the four things that make a spike worse than steady load: cold caches on new instances raise per-request cost; the autoscaler needs minutes while the spike lasts seconds; a connection storm from new instances multiplies pool size by instance count (Module P-4); and every client that got an error retries at once (Module A-6).

Recovery tests are the ones nobody runs and everybody needs. Module A-8's metastable failure state is precisely the case where removing the cause does not fix the system, and the only way to know whether yours exits on its own is to drive it into overload, stop, and watch. If it does not recover without a human, that is a finding worth more than the throughput number.


The Methodological Error That Invalidates Most Load Tests

Closed-loop generators cannot observe overload

Two ways to generate load:

Closed loopN virtual users, each sending a request, waiting for the response, then sending the next. This is the default model in many tools, and it has a fatal property:

text

The generator is self-throttling, and it is negotiating with your system rather than testing it. Real users do not behave this way: a browser's arrival rate does not fall because your server is slow — if anything, users reload and it rises (Module A-6).

Open loop / arrival rate — the generator sends at a specified rate regardless of response times, so queues build exactly as they would in production. This is the only model that can find the knee, observe the failure mode, or test admission control.

If your load test cannot make the system fail, check whether the generator is closed-loop before concluding the system is robust.

Coordinated omission

The related measurement error, and it makes reported latency wildly optimistic. In a closed-loop test, when the system stalls for two seconds, the virtual user is waiting — so it does not send the forty requests it would have sent during that stall, and therefore does not record forty slow measurements. The stall is represented by one sample instead of forty.

text

The fix is either an open-loop generator that records the intended send time (so latency is measured from when the request should have gone out) or a tool that explicitly corrects for it. Without one, your p99 is a description of the requests that got through, which is not the same as your users' experience.

The test must resemble reality in three specific ways

The traffic mix. Hammering one endpoint measures that endpoint. A realistic mix — the actual ratio of reads to writes, cheap to expensive endpoints, authenticated to anonymous — is what reveals contention between them, which is where the interesting failures are (Module P-8's noisy neighbour, Module A-7's shared pool).

The key distribution. This is the one most often wrong. A test that requests the same twenty entities achieves a near-100% cache hit ratio and measures your cache. Real traffic has a long tail, so the working set is far larger than the hot set and the hit ratio is much lower (Module F-12). Generate keys with a realistic distribution — typically Zipfian — or you will size your cache and database from a fiction.

The data volume. A query that is fast on 100,000 rows is not fast on 400 million: different plan, different index depth, different buffer-cache behaviour (Modules F-13, P-22). This is Module O-3's structural-versus-behavioural parity point, and it is why load testing against a small staging dataset produces numbers that do not transfer.

Also: discard the warm-up. JIT compilation, cache fill, connection-pool growth and autoscaling all make the first minutes unrepresentative. Measure steady state, and separately measure the warm-up, because that is the spike behaviour.


Testing in Production, Carefully

Staging gives structural parity. Only production has the real data volume, the real traffic mix, the real dependency behaviour and the real infrastructure. So the highest-fidelity techniques run there:

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.