System Design Capacity
Auto-scaling
Friday night in a big city: Uber ride requests spike as people leave work and bars fill up. The matching API that felt quiet at 2 PM suddenly needs many more replicas (copies of the service running side by side), and it needs them before every open request times out.
is the control loop that adds capacity when load rises and removes it when quiet returns. Done well, it keeps enough (spare capacity) to absorb a surge, and enough patience that it does not add and remove servers every minute. Done badly, it reacts late, scales on the wrong signal, or floods the database behind it with new connections. This page walks through how the loop works, how to pick the signal and policy, how to size the numbers, and what to do in the minutes before new capacity is ready.
Decision panel
Which signal?
CPU, p99 latency, queue lag, or connections: pick the one users feel first.
vs
Wait for a threshold breach, or pre-warm from the calendar before it?
Min / max bounds
Enough minimum to survive one zone loss; a maximum that protects dependencies.
What protects users during the minutes before new pods are ready?
What actually happens
Autoscaling is a feedback loop, not magic. A metric such as CPU or request latency feeds a policy. The policy decides to (add replicas) or (remove them). The new capacity changes the next metric reading, and the loop runs again. Three mechanisms keep that loop from oscillating: a pauses between actions so the metrics can settle, time is budgeted so half-ready replicas are not counted as capacity, and draining lets a replica finish its in-flight work before it is removed.
The loop is easy for a (any replica can answer any request because sessions and data live in shared stores). Databases and stateful workers need different primitives: read replicas, partition-aware autoscalers, or scaling consumers on (how long work has been waiting). Whatever the tier, start conservatively with fixed min and max bounds; the policy section below explains how to pick the signal and what to alarm on.
The rest of the page follows that loop in order: which signal to feed it, whether to react or predict, how to size the thresholds from real capacity numbers, and how to survive the gap between "metrics say scale" and "new replicas are useful".
Try it: autoscaling cooldown and warmup
Raise CPU past the target, then advance simulated time. Scale-out starts warmup and cooldown timers; traffic still lands on ready replicas until warmup completes. Scale-in waits for low CPU and an empty cooldown.
Replicas
3 ready / 3 total
Cooldown
0s
Warmup
0s
- CPU
- 48%
- Headroom to target
- 7%
- Min / max
- 2–10
- Scale events
- 0
48% CPU · 3/3 ready · cooldown 0s · warmup 0s · 0 scaling actions. Fleet is steady at 48% CPU on three ready replicas. Raise load to cross the target and trigger scale-out.
Plan for lag. Metrics, policy decision, instance boot, and readiness probes can take minutes. Pre-scale before known events; do not count warming pods as capacity.
How to choose a scaling policy
With the loop in mind, three questions decide which metric feeds it and how aggressive the policy should be.
1. What hurts the user first? Compute-bound APIs (where the CPU is the bottleneck) can track CPU. I/O-bound APIs that spend their time waiting on a database need (the slowest 1% of requests) or queue age instead, because CPU can stay low while users already suffer.
2. Is the spike on the calendar? Recurring peaks deserve (pre-warming from a schedule or forecast) with reactive guards underneath. Surprises need thresholds and (dropping or degrading low-priority work) to cover the minutes of provisioning lag.
3. Can dependencies absorb more replicas? Every new app pod opens its own database connections, so more pods can multiply pressure on a database that did not grow. Cap connection pools, add backpressure (make callers slow down instead of piling up), and alert on shared dependency saturation before raising the maximum replica count.
Simple rule of thumb
Prefer (hold one metric near a setpoint, like a thermostat) when your cloud supports it cleanly. Scale out in roughly a minute and scale in over several minutes, keep hard min and max caps, page a human when the fleet sits at max replicas, and pre-scale whenever takes minutes rather than seconds.
The feedback loop: reactive or predictive
The three questions above pick the inputs; this is the loop they feed. Metrics come in, a capacity change goes out, and the loop observes again. Notice the amber steps on each branch: scaling out is not finished when a pod starts but when its caches and connections are warm, and scaling in is not finished until in-flight work has drained.
Metrics: CPU, latency, queue lag
Scaling policy
Need capacity change?
Scale out
Add pods or instances
Warm caches and connections
Scale in
Drain and remove slowly
Finish in-flight work
No
Keep observing
Loop back to metrics
The decision diamond can be triggered in two ways. waits until a threshold breaches and then provisions, which is honest about real demand but can cost minutes of lag. uses calendars or models to pre-warm before the spike arrives. The two cards compare what each one triggers on and where it falls short.
Reactive scaling
Trigger: a metric crosses a threshold now
- CPU above 70% for 3 minutes adds 2 instances, a common web-tier rule.
- Queue depth above 1000 messages adds 5 workers so the backlog clears sooner.
- p99 latency above 500 ms adds instances because users already feel the wait.
Strengths
- Simple to configure and explain, so on-call engineers can reason about it.
- Responds to actual demand rather than to a forecast that might be wrong.
- Needs no historical data, which makes it the right starting point for new services.
Trade-offs
- Reactive lag of 3-7 minutes to provision, during which users keep waiting.
- New instances need warmup, so the raw pod count overstates real capacity.
- Oscillation risk without a cooldown: capacity added and removed every few minutes.
Predictive scaling
Trigger: a forecast predicts future load
- Historical pattern: every Monday 9 AM brings 3x load, so capacity is added beforehand.
- Seasonal event: an annual sale runs at 20x baseline, far beyond what reactive catches.
- Event-driven: a game launch can spike 100x, which no threshold catches in time.
Strengths
- Zero reactive lag, because capacity is already warm when the spike lands.
- Handles predictable traffic patterns without permanently over-provisioning for them.
- Combines with reactive guards so unknown spikes are still covered.
Trade-offs
- Needs historical data, so a brand-new service has a cold start problem.
- Cannot handle novel events alone; keep reactive thresholds underneath it.
- May over-provision when the forecast is wrong, which wastes money.
Scaling metrics
A scaling metric should respond predictably when replicas are added. Average CPU often falls when parallel CPU work is distributed, but a downstream database bottleneck may keep latency high regardless of application replica count. Queue age signals user delay, while queue depth and arrival rate help estimate the work required to remove it. Use the delay to detect pain and a capacity model to choose the replica count.
Whichever trigger style you use, the loop is only as good as the metric feeding it, and scaling on CPU alone can miss queue backlogs, downstream saturation, or spikes. Pick the signal that matches your bottleneck: CPU for compute-heavy APIs, for consumers (the age of the oldest job often matters more than raw depth), p99 when users feel pain before averages move, and active connections for WebSocket servers (long-lived two-way connections). The last row shows a composite scale-in rule: shrinking only when several signals agree is how you avoid deleting capacity on a brief dip.
| Metric | Best for | Threshold example |
|---|---|---|
| CPU utilization | Compute-bound services | Scale out at 70%, in at 30% |
| Memory usage | In-memory processing, caches | Scale out at 80% RSS; scale in only when sustained low |
| Request count | Stateless web servers | Target 1000 req/sec per instance |
| Response latency | User-facing services | Scale when p99 > 200 ms |
| Queue depth | Worker / consumer services | Scale when depth > 500 |
| Active connections | WebSocket / SSE servers | Scale at 50K conn per instance |
| Custom business KPI | Domain-specific signals | Orders/min, concurrent games |
| GPU utilization | Accelerated inference services | Scale at 80% GPU |
| Composite scale-in | Avoid flapping / premature shrink | Scale in only when CPU < 30% AND p99 < 50 ms AND queue < 10 (example) |
Worked example: numbers to know
Assume each ready worker completes 40 jobs per second, arrivals are 240 jobs per second, and 12,000 jobs are waiting. Six workers only match arrivals and never clear the backlog. To clear it in five minutes, add 12,000 / 300 = 40 jobs per second of spare processing capacity, requiring seven workers in total under this simple model. Provisioning delay, retries, and uneven job sizes require additional reserve or a revised drain target.
A threshold like "70% CPU" means nothing until it connects to a capacity model. The worksheet below runs one million-user API through the steps: pick the metric closest to pain, load-test the safe per-replica capacity, divide peak QPS (queries per second) by it, add for surges, reserve capacity for a zone failure, then set cooldowns around the real warmup time. Thresholds should come from load tests and user-facing latency targets, not from round numbers, and later sections refer back to these figures rather than repeating them.
| Design step | Example | Policy choice |
|---|---|---|
| Metric closest to pain | QPS, CPU, p95/p99, queue age, connection count | Avoid scaling on CPU alone for I/O-bound APIs. |
| Safe per-replica capacity | Load test shows p99 rises after 120 QPS/replica | Set target around 90-100 QPS/replica. |
| Peak target | 1.2K peak QPS ÷ 120 safe QPS | 10 replicas before headroom. |
| Headroom | 10 × 1.3 | 13 replicas at shared peak. |
| Failure reserve | Three zones with one zone loss | Keep normal utilization at or below about 66%. |
| Warmup and cooldown | Cold start + readiness takes 3 minutes | Pre-scale before known spikes; scale in slowly. |
Two details the table compresses. First, the per-replica target is usually a pair: keep each replica near 120 QPS or 65% CPU, whichever trips first, and the peak figure comes from converting a daily request total into a peak rate. Second, the failure reserve is why normal utilization stays well under 100%: if one of three zones disappears, the remaining two must carry everything, which is where the utilization cap in the table comes from.
The anti-pattern this worksheet guards against: CPU stays low while p99 is high because replicas are waiting on the database, so a CPU-only policy never fires. When it finally does fire, the dependency trap from question 3 of the policy section applies and the incident gets worse. Scale out on p95/p99 or queue age when CPU is misleading, and scale in only after sustained low traffic and successful drains.
The last worksheet row (cold start plus readiness) is worth drawing out, because it is the gap every policy must budget for. Two things happen at once during that gap:
Traffic spike starts
Capacity lag path
Metrics breach after 1–2 min
New replicas start
3 min cold start and readiness
Capacity becomes useful
User-pain path
p99 rises during lag
Load shedding or degraded mode protects dependencies
Autoscaling lag and degraded mode
The delay includes collecting a metric window, evaluating policy, scheduling instances, obtaining machines if needed, starting the application, warming dependencies, and passing readiness checks. During that interval, arrivals continue. At 1,000 requests per second above current capacity, a two-minute delay represents 120,000 excess requests. A finite queue cannot make that demand disappear; decide which requests wait, receive a cheaper response, or are rejected.
The two paths in the diagram above run at the same time: capacity is on its way, and users are already hurting. Autoscaling reacts after metrics move, so during the detection, provisioning, and warmup gap the system needs admission control (deciding which requests to accept at all) and so p99 does not spiral. Think of it as four phases.
Before the spike
Pre-scale known peaks when cold start plus readiness takes minutes. Warm caches, connection pools, and JIT paths (code the runtime compiles on first use) before the traffic arrives.
During lag
Shed low-priority routes, shorten expensive fan-out (one request calling many services), serve stale cache where safe, and queue asynchronous work instead of letting every request wait.
After scale-out
Ramp traffic to new replicas slowly, and keep watching the shared dependencies from question 3 of the policy section, because they did not grow with the fleet.
Test the policy
Run a step-load test from 231 average QPS up to the peak in the worksheet, and verify p95/p99, scale-out time, max-replica alarms, and scale-in drain behaviour.
Max-replica incident mode
If the service reaches max replicas and p99 still rises, the loop has run out of room. Page humans and switch to the overload policy from the "during lag" phase: shedding, feature degradation, queue backpressure, and dependency protection. Raising the maximum blindly only moves the bottleneck onto the database.
How platforms implement the loop
In Kubernetes, HPA changes desired pod replicas, while node provisioning is a separate capacity concern. A desired pod that remains pending adds no serving capacity. CPU utilization targets are measured relative to configured CPU requests, so unrealistic requests distort the policy. Watch desired, scheduled, ready, and actually serving replicas separately to see which stage prevents scaling from helping. See Kubernetes HPA documentation for the precise contract.
Everything so far is policy; this is the machinery that runs it. On Kubernetes, the changes the replica count from CPU or custom metrics, the changes each pod's CPU and memory requests instead (and usually restarts the pod to apply them), and an event-driven autoscaler scales from external signals such as stream lag or HTTP queue depth, including down to zero for batch workloads. When those want more pods than the nodes can hold, a cluster autoscaler adds nodes, and modern provisioners can choose suitable instance types quickly. On cloud instance groups the same ideas appear as (the recommended default), step scaling for graduated responses to severe breaches, simple scaling for one action at a time, scheduled scaling for predictable calendars, and that stop the group thrashing between add and remove.
1. Horizontal pod autoscaler (Kubernetes)
The manifest below bounds replicas between 10 and 500 and scales on two metrics at once: average CPU utilization at 70% and 1000 requests per second per pod. Set resource requests accurately, prefer custom metrics over CPU when you can, and use a scaleDown stabilization window so a brief dip does not trigger premature scale-in.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
scaleTargetRef:
kind: Deployment
name: api-server
minReplicas: 10
maxReplicas: 500
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000"2. Vertical pod autoscaler
- Adjusts CPU and memory requests per pod, right-sizing instead of adding replicas.
- Avoids waste from over-provisioned pods that reserve resources they never use.
- Restarts pods to apply changes, so set disruption budgets (a cap on pods down at once).
3. Event-driven autoscaler
- Scales on external event sources rather than on the pods' own resource usage.
- Typical signals are stream consumer lag, queue depth, and custom metric queries.
- Can scale to zero, so idle batch workloads stop costing money entirely.
- Also covers cron-based pre-scale for known events and queue depth for async workers.
4. Target tracking (recommended default)
- Ask for "keep average CPU at 60%" and the group adjusts capacity like a thermostat.
- Best for steady-state services with one clear metric target and predictable behaviour.
5. Step scaling (graduated response)
- CPU at 60-70% adds 1 instance, a gentle response to a mild breach.
- CPU at 70-80% adds 3 instances, and above 80% adds 5, matching severity.
- Best for services where spike severity varies and one fixed step is wrong.
6. Simple scaling (single action)
- CPU above 70% adds 2 instances, then a 300 second cooldown before anything else.
- Simplest policy but slowest, because it waits out the full cooldown between actions.
- Best for stable workloads where scaling events are infrequent and predictable.
7. Scheduled scaling
- Scale to 50 instances at 8:55 AM EST, just before business hours begin.
- Scale back to 10 instances at 8:00 PM EST when the office day is over.
- Best for predictable daily or weekly patterns you can put on a calendar.
Cooldown periods
- Default of 300 seconds (5 minutes) prevents oscillation between add and remove.
- Scale-out cooldown of 60-120 seconds, because adding capacity late costs users.
- Scale-in cooldown of 300-600 seconds, because deleting capacity early is riskier.
- Warm-up is the time a new instance needs to reach full capacity; budget it too.
Databases vs stateless services
All the machinery above assumes the thing being scaled can be copied freely. That is true for a web tier, where any replica can answer if session state lives elsewhere, and false for a data tier, which carries partitions (slices of the data pinned to nodes) and replication lag. Managed storage scaling exists, but migration is never "just add pods."
Stateless services (easier)
API servers, web frontends, workers
- Add or remove instances freely, because no replica owns anything unique.
- No data migration is needed when the fleet grows or shrinks.
- A health check passes and the replica is ready in seconds.
- Going from 10 to 100 replicas can take under 2 minutes.
Prerequisites
- Externalize state into a cache or database so replicas stay interchangeable.
- Externalize sessions with signed tokens or a shared cache, never local memory.
- Shared-nothing architecture, so no replica depends on another replica's disk.
- Graceful shutdown that drains connections, so scale-in does not drop requests.
Databases (harder)
Why it is different
- Read replicas are the easy part: add copies that serve reads.
- Write scaling means sharding (splitting data across nodes) or multi-master replication.
- Data migration means hours or days of rebalancing, not a readiness probe.
- State is pinned to nodes, so you cannot add or remove them freely.
Strategies
- Storage autoscaling grows disk automatically for datasets that keep expanding over time.
- On-demand read and write capacity for key-value stores, provisioned as demand arrives.
- Proxy-assisted sharding for relational databases, so the app sees one endpoint.
- Distributed SQL with automatic shard rebalancing when nodes are added.
- Read replicas added automatically under load, the closest thing to web-tier scaling.
Cost optimization
Scale-in is a controlled removal, not just a smaller number. Stop routing new requests, drain bounded work, checkpoint or release worker ownership safely, and let persistent clients reconnect with backoff. Stabilization windows reduce repeated add-remove cycles around a threshold. Keep enough minimum capacity for expected bursts and failure recovery; the lowest idle bill is not necessarily the lowest total cost when repeated cold starts cause incidents.
Scaling in is where the savings live: auto-scaling saves money only when idle capacity actually disappears. Combine spot or preemptible nodes (spare cloud capacity sold cheaply but reclaimable at short notice) for fault-tolerant work, right-sizing for steady waste, and a reserved baseline (capacity committed for a year or more at a discount) for predictable cores.
1. Spot / preemptible instances (60-90% cheaper)
- Use for stateless workers, batch processing, and dev or staging environments.
- Mix roughly 70% on-demand for the baseline with 30% spot for burst capacity.
- Diversify capacity pools so one interruption source does not drain all workers.
2. Right-sizing (VPA or manual analysis)
- Most services are over-provisioned by 40-60%, so this is usually the first win.
- Analyze actual CPU and memory usage over 14 days before changing shapes.
- Downsizing instance shapes, for example large to medium, produces meaningful savings.
3. Scale-to-zero
- Function or container platforms suit bursty workloads that sit idle most hours.
- Event-driven autoscaling can take a workload to 0 pods when no events arrive.
- Turning dev and staging environments off at night saves 60% or more.
4. Reserved capacity (baseline)
- Reserve instances for the minimum steady-state load you know you will always run.
- A 1-year term gives a 30-40% discount; 3-year terms give 50-60%.
- Auto-scale on top of the reserved baseline with on-demand instances for peaks.
Large media model
- Baseline runs on reserved instances, because daytime load is predictable.
- Evening peak of 3-4x baseline runs on on-demand scaling instead.
- Encoding runs on spot instances, because batch jobs tolerate interruption.
- Result is roughly 40% cost reduction versus running everything on-demand.
Failure timeline, and what must never become false
Putting the pieces together, here is the Friday spike from the top of the page, minute by minute, using the figures from the worksheet above. The in the timeline is the Kubernetes autoscaler from the platform section. The invariant that follows is what every fix on this page is protecting.
Timeline: reactive lag on a Friday spike
t=0 Steady 231 avg QPS; 13 replicas sized for ~1.2K peak with headroom t=1m Spike to 1.2K; CPU still looks OK while p99 rises (I/O wait) t=3m HPA finally trips; new pods start; 3 min cold start + readiness t=4m Users feel pain; DB connections multiply as half-ready pods join t=5m Without shedding: cascade. With shedding + predictive pre-warm: survive Outcome Autoscaling is late by definition: budget the gap and guard dependencies
Correctness invariant
Scale-out must never exceed what shared dependencies can absorb: database pools, cache QPS (queries per second), and downstream request rates. During detection plus lag, protect the platform with rather than unbounded queues. And when the fleet is at max replicas with rising p99, the answer is the incident mode described above, not a blind raise of the ceiling.
Wrap-up
You have seen the loop, the signals, the numbers, and the lag gap. Use this section to rehearse the ideas for an interview, check them with the exercises, and follow the references for primary sources.
How to explain this in an interview
Lead with the signal: name the metric closest to user pain (the scaling metrics table above) and say why CPU alone misleads I/O-bound services. Then name the policy (reactive, predictive, or both), the min and max bounds with the zone-loss reserve, and what protects users during warmup lag. Expect these follow-ups:
- How long does a new instance take to become useful, and is that budgeted?
- What minimum capacity survives one zone failure without breaching latency targets?
- Can scale-in kill long-running jobs or open WebSocket sessions before they drain?
- What happens when we hit max replicas during an incident and p99 still climbs?
Exercises
- Safe capacity is 100 QPS per replica instead of the worksheet's 120; keep the same peak, headroom, and one-of-three zone loss. Compute the minimum peak replicas and the normal utilization cap.
- CPU stays at 40% while p99 is 800 ms waiting on the database. Do you scale the API? Which metric and which dependency guardrail do you set first?
- Warmup takes as long as the worksheet says, and a known sale starts at 20:00. Design the predictive plus reactive policy and the load-shed behaviour for the lag window.
References
- Kubernetes Horizontal Pod Autoscaler and VPA documentation: metrics, stabilization windows, and disruption budgets.
- AWS Auto Scaling and target tracking docs: cooldowns, predictive scaling, and scheduled actions.
- Google SRE Book, Managing Load and Handling Overload: capacity planning and load shedding.
What to remember
Scale on the signal closest to user pain, not the metric that is easiest to graph. Pre-warm the peaks you can see coming and budget the lag for the ones you cannot. And keep the invariant in view: more Uber matching pods only help if the databases behind them can keep up.
Practice and further reading
Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.
Focus for this lesson: Scale from demand and queueing signals with warm-up, stabilization, safe scale-in, dependency caps, and policy load tests.
Explain the decision
Why is CPU alone often a poor autoscaling signal?
Read the reasoning
Queued work, request concurrency, latency, memory, and downstream saturation can lead or contradict CPU; choose a signal proportional to unmet demand.
Apply it to a production scenario
A queue-based scaler oscillates between 20 and 200 workers. What do you tune?
Read one approach
Arrival/service estimates, target backlog age, warm-up delay, stabilization windows, step size, cooldown, and safe termination of in-flight work.
Work through the numbers
Jobs arrive at 3,000/min and one worker completes 20/min at target utilization. Estimate workers plus 25% headroom.
Read the worked calculation
150 workers at steady state; about 188 with headroom, then cap against database and external quotas.
Reason through a failure
What must scale-in preserve?
Read the recovery reasoning
No accepted work is abandoned: drain connections, checkpoint or requeue jobs, respect disruption budgets, and remove capacity gradually.
Sources and further study
Continue with Latency vs Throughput.