Capacity planning turns a demand forecast into a provisioned operating envelope. The useful question is not how many requests a machine can accept before crashing, but what workload the service can sustain while meeting its latency and correctness requirements. That answer depends on request cost, dependencies, and failure assumptions. A spreadsheet becomes useful when each of those assumptions has a source and an owner.

Measure an acceptable operating point

Load tests should use a representative request mix, data size, cache state, concurrency, and software configuration. Increase demand while recording latency, errors, CPU, memory, queue depth, storage, and connection usage. The highest tested point meeting the chosen requirements is evidence for that workload, not proof that every intermediate point or future release behaves identically. A throughput result without outcome quality can reward a server that rejects work quickly.

Track different bottlenecks separately. More application replicas may increase CPU capacity while leaving a shared database connection limit unchanged. Expensive requests can also change the relationship between requests per second and resource consumption. A request-rate model is reasonable only while that cost distribution is comparable; monitor the underlying resources and revise the model when the workload changes. Do not project a quiet average into a peak-capacity promise.

Forecast demand and remove capacity deliberately

Start with an observed peak, then state the growth or seasonal scenario being planned. Add provisioning lead time and uncertainty explicitly. The operating reserve should cover a named reason, such as load imbalance, forecast error, or time needed to add instances, rather than being a mysterious percentage inherited from an old spreadsheet. Revisit both the measurement and the demand assumption after meaningful changes.

Next remove the capacity that the reliability design says may fail. A service spread across three zones needs a different instance count if it must serve the same peak with one zone unavailable. Account for which dependencies survive, how traffic moves, and whether enough instances can start before queues overflow. Balanced placement does not by itself guarantee balanced demand, and autoscaling cannot supply capacity instantaneously.

When it breaks

A common estimate divides forecast traffic by a single-machine maximum, rounds up, and stops. It may satisfy a healthy-cluster calculation while failing immediately after a zone loss. Counting reserved capacity twice is another error: distinguish a tested throughput point from the lower rate chosen for normal planning. State which boundary a reported headroom number uses so that spare capacity is not confused with physical maximum throughput.

Linear scaling can fail at a shared lock, database, network link, or external quota. Cold caches and synchronized retries can make a failover workload more expensive than the original benchmark. Validate the failure scenario under realistic conditions and record where the model stops applying. If a forecast cannot be served safely, admission control or a tested degraded mode may be necessary while provisioning catches up; adding an optimistic cell to the plan will not protect users.

Check it quickly

Save this as capacity.py and run Python 3. The inputs are synthetic load-test results for a fixed workload. This exercise verifies the sizing arithmetic and failure assumption, not the performance of your hardware. A planning rate of 70% of the accepted test point is an explicit example choice, not a universal utilization recommendation.

from math import ceil

# Synthetic load-test results for one fixed request mix and machine size.
samples = [
    {"rps": 150, "p99_seconds": 0.11, "error_ratio": 0.0001},
    {"rps": 200, "p99_seconds": 0.22, "error_ratio": 0.0003},
    {"rps": 250, "p99_seconds": 0.50, "error_ratio": 0.0020},
]
acceptable = [s["rps"] for s in samples
              if s["p99_seconds"] <= 0.30 and s["error_ratio"] <= 0.001]
tested_rps = max(acceptable)
forecast_rps = ceil(1600 * 1.25)
zones = 3
surviving_zones = zones - 1
planning_rps = tested_rps * 0.70

naive_per_zone = ceil(ceil(forecast_rps / tested_rps) / zones)
planned_per_zone = ceil(forecast_rps / (planning_rps * surviving_zones))
naive_survivor_rps = naive_per_zone * surviving_zones * tested_rps
planned_survivor_rps = planned_per_zone * surviving_zones * planning_rps

assert tested_rps == 200 and forecast_rps == 2000
assert naive_per_zone * zones == 12 and naive_survivor_rps == 1600
assert planned_per_zone * zones == 24 and planned_survivor_rps == 2240
assert naive_survivor_rps < forecast_rps <= planned_survivor_rps
print(f"tested={tested_rps} rps/node; forecast={forecast_rps} rps")
print(f"naive={naive_per_zone * zones} nodes; zone-loss capacity={naive_survivor_rps} rps")
print(f"planned={planned_per_zone * zones} nodes; zone-loss planning capacity={planned_survivor_rps:.0f} rps")
print(f"zone-loss spare={planned_survivor_rps - forecast_rps:.0f} rps")

The accepted listed point is 200 requests per second per node; the 250-request sample fails the quality requirements. Forecast demand becomes 2,000 requests per second. The naive balanced allocation uses twelve nodes but leaves only eight after a zone loss, providing 1,600 requests per second even at the accepted test rate.

The reserve-aware plan allocates eight nodes per zone, twenty-four total. Sixteen survivors at the lower planning rate provide 2,240 requests per second, leaving 240 below that operating limit. This result depends on the fixture’s fixed request mix, independent node scaling, and surviving dependencies. Replace those assumptions with measured evidence before turning the number into a purchase or a reliability commitment.

Further reading

Sources