Capacity planning and performance
Generated by Claude through GigAI (karthik446/gigai-learning-center 4c0cb514) on 2026-10-13.
Concept: Capacity planning and performance · also covers Scaling, Workload scheduling and capacity management, Kubernetes · Lesson 14 of 31 in System design · version 1
Practice thisOn this page
Capacity planning turns a demand forecast into a resource plan with known headroom, and performance work finds the limit of each component before production traffic does. The loop is: model the load, measure what one instance can sustain, find the bottleneck, then decide how the system grows, by hand or by autoscaling. Estimates start the conversation, but load tests settle it.
Read more
- docLocust documentation (docs.locust.io)
- docWriting a locustfile (docs.locust.io)
- docLocust distributed load generation (docs.locust.io)
How it is done in practice
A team keeps a short capacity document per service: demand assumptions, measured throughput per instance, the known bottleneck, and the headroom policy. Before a launch or a major change they run ramp and soak tests from a distributed load generator against a production-like environment, with dashboards open on every tier. The results set replica minimums, autoscaler targets and pool sizes. Quarterly, the forecast is compared with production metrics and procurement or quota requests are raised for tiers with long lead times.
Technologies
Diagram
flowchart LR F[Demand forecast] --> M[Capacity model] M --> T[Load test] T --> B[Find bottleneck] B --> X[Fix or scale] X --> T T --> P[Per instance throughput] P --> M M --> A[Autoscaler targets and replica bounds] A --> O[Production metrics] O --> F
flowchart LR F[Demand forecast] --> M[Capacity model] M --> T[Load test] T --> B[Find bottleneck] B --> X[Fix or scale] X --> T T --> P[Per instance throughput] P --> M M --> A[Autoscaler targets and replica bounds] A --> O[Production metrics] O --> F
Code
pythonfrom locust import HttpUser, task, between
class Shopper(HttpUser):
wait_time = between(1, 3)
@task(5)
def browse(self):
self.client.get("/items")
@task(1)
def checkout(self):
self.client.post("/orders", json={"sku": "A1", "qty": 1})
# locust -f locustfile.py --headless -u 500 -r 50 -t 10m --host HOST
yamlapiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 4
maxReplicas: 40
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
Subtopics
From demand model to resource plan
Modelling the load
Start from business drivers: active users, actions per user, payload sizes and growth rate. Convert them into peak request rate per endpoint, not the daily average, since peak hour is commonly several times the mean and launches or batch jobs add spikes. Split reads from writes and cheap calls from expensive ones, because one heavy endpoint can dominate. For each tier, derive throughput, storage growth per month, memory for working sets and caches, and network transfer. Record the assumptions so they can be checked against production metrics later.
Headroom and failure
Divide peak demand by the measured sustainable throughput of one instance to get a base count, then add headroom. Plan for N plus one or N plus two: the fleet must carry peak load with a node, or a whole zone, gone, and during a deploy that takes instances out of rotation. Run at a target utilisation well below saturation, since latency rises sharply as utilisation approaches one hundred percent. Stateful tiers need longer lead time than stateless ones, as adding a database shard or replica involves data movement.
Load testing
Shaping a realistic test
A load test is only as good as its workload model. In Locust, behaviour is plain Python: a user class holds tasks, weights set how often each task runs relative to the others, and a wait time between tasks models think time. Each simulated user runs in a lightweight greenlet, so one process can drive many concurrent users. Use realistic data variety, since hitting one cached key measures the cache and nothing else. Run the test against an environment with production-like data volume and the same topology, including the load balancer and connection pools.
Kinds of test and scaling the generator
Run several shapes: a ramp to find the breaking point, a soak at expected peak for hours to expose leaks and slow growth, and a spike to see recovery. Watch the load generator itself, as a saturated generator under-reports throughput and distorts latency. Locust scales out with one master process that coordinates and aggregates statistics and many worker processes that generate the load, typically one worker per CPU core across several machines. Run it headless in CI with a fixed user count, spawn rate and duration to catch regressions.
Finding the bottleneck
Reading the results
Plot throughput and latency percentiles against offered load. In the healthy region throughput rises linearly and latency is flat. At the knee, throughput flattens and tail latency climbs: some resource is saturated. Identify it by checking utilisation, saturation and errors for each resource: CPU, memory, disk IO, network, and also software limits such as thread pools, connection pools, file descriptors and locks. The limiting resource is frequently a downstream dependency or a pool size, not CPU. Report the ninety-ninth percentile, not the mean, and note that fan-out calls inherit the tail of every backend they touch.
Acting on it
Fix the one bottleneck, retest, and expect the next one to appear. Typical remedies in order of cost: remove wasted work such as N plus one queries or chatty calls, add caching or batching, tune pool sizes and timeouts, scale the tier out, and finally change the architecture by partitioning or moving work to asynchronous processing. Keep the per-instance sustainable throughput figure from each test, as it is the main input to the capacity model.
Autoscaling and its limits
How horizontal autoscaling decides
The Kubernetes HorizontalPodAutoscaler is a control loop that periodically compares an observed metric with a target and sets the replica count to roughly the current replicas multiplied by the ratio of current to desired metric value, rounded up. Metrics can be resource based, such as average CPU utilisation relative to requests, or custom and external, such as requests per second or queue length. A tolerance band avoids reacting to small deviations, and a scale-down stabilisation window prevents flapping by using the highest recent recommendation. Minimum and maximum replica bounds cap the behaviour.
Design consequences
Autoscaling reacts after load arrives, and new capacity takes time: scheduling, image pull, startup, warm-up and readiness. Keep enough headroom to absorb a spike for that long, or scale on a leading signal such as queue depth. Scaling a stateless tier can simply move the bottleneck to a database that does not scale the same way, exhausting its connections. Set correct resource requests, since utilisation targets are computed from them, and pair autoscaling with load shedding for demand beyond the maximum.
Sources
- docLocust documentation (docs.locust.io)
- docWriting a locustfile (docs.locust.io)
- docLocust distributed load generation (docs.locust.io)
- docKubernetes Horizontal Pod Autoscaling (kubernetes.io)