Autoscaling simulator
An autoscaler adds servers when load rises and removes them when it falls. Here, traffic hits a Kubernetes Deployment and the Horizontal Pod Autoscaler, running the same algorithm as the real controller, decides every 15 seconds how many pods to run. Watch how far it lags behind a spike, and why.
ready pods replicas the HPA asked for pods the traffic needs at 50%
pending starting ready
- Traffic needs
- 0.0 CPU cores
- Each pod requests
- 500m, can use 1000m
- Demand not served so far
- 0.0%
- –The first check runs at 0 min 05 s.
How to use the simulator
Simulated time runs at 30 times real time. Traffic needs a certain amount of CPU, which the ready pods share equally; each pod requests 500 millicores (half a core) and can use up to 1000. The HPA wants average usage at the target percentage of the request, between 2 and 20 pods. The dashed line shows how many pods the traffic needs at the target, the solid line what the HPA asked for, and the shaded area the pods actually ready. Each decision in the log shows the calculation behind it.
Try these:
- Run the Spike. Traffic jumps fivefold at 2 minutes. The HPA's first answer is capped by its rate limit, the next is damped because the new pods are still starting, and capacity catches up after about 80 seconds. Then switch to Slow start-up and compare how much demand goes unserved.
- Watch the end of the spike at 10 minutes. With the default 300-second window, pods stay for five more minutes. Set the window to 0 s and they go at the next check.
- Choose Burn CPU, so starting pods use their full CPU limit. Nothing changes, because the HPA ignores CPU readings from pods that are still starting.
- Raise the target to 90%. Fewer pods run, but there is little headroom, so spikes go unserved for longer.
The controller is a port of the Kubernetes source (the replica calculator and the stabilization and rate-limit functions), checked against the worked examples in the Kubernetes documentation. The traffic, start-up times and the way pods share load are a simple model, described in the article.
What is autoscaling?
Autoscaling changes the amount of computing capacity a service runs on as its load changes. Horizontal autoscaling adds or removes copies of the service (pods, containers, virtual machines); vertical autoscaling gives each copy more or less CPU and memory. Horizontal scaling is the common case for stateless services, because copies can be added without restarting the ones already running.
Most autoscalers are feedback loops. They measure something that rises with load, such as CPU utilization, requests per second or queue length, compare it with a target, and change the number of copies to bring it back. The hard part is that capacity takes time to arrive: a new copy has to be scheduled, started and warmed up before it can take traffic, and by then the load may have moved on.
How the HPA decides
The Kubernetes Horizontal Pod Autoscaler is a control loop in the kube-controller-manager. Every 15 seconds it reads each pod's CPU usage from the metrics API, usually served by metrics-server, and expresses it as a percentage of the pod's CPU request. It averages that over the pods with usable readings and computes
desiredReplicas = ceil(currentReplicas × currentUtilization ÷ targetUtilization)
If the ratio of current to target is within 10% of 1.0, it does nothing, so small fluctuations do not cause churn. In the steady scenario, traffic needs 1.2 cores and each pod should run at 50% of half a core, so 4.8 pods would be exact; the HPA settles on 5, the next whole number. A pod without a CPU request has no utilization, and if any pod lacks one the HPA takes no action on CPU.
Two details from the source code show up in the log: utilization is rounded down to a whole percentage before the ratio is taken, and the multiplication uses the number of pods with usable metrics, not the number the Deployment asks for.
Starting pods
New pods are a problem for any CPU-based autoscaler. A pod that is still starting serves no traffic, so counting its low CPU would drag the average down and slow the scale-up; an application that burns CPU while it starts, such as a JVM compiling code, would push the average up and cause more scaling than needed.
The HPA handles this in two ways. First, for the 5 minutes after a pod starts (the CPU initialization period), it ignores the pod's CPU unless the pod is ready and the latest sample was collected entirely after it became ready. A pod that is still Pending, or not yet ready, is set aside. Second, if the remaining pods say "scale up", the set-aside pods are added back as if they used 0% of their request, and the ratio is recomputed. That damps the step while new pods are on their way: if they are already enough, the HPA waits instead of adding more.
Both effects appear in the first two decisions after the spike in the simulator. At 2 min 20 s the HPA wants 16 pods but the rate limit allows 8. At 2 min 35 s, with 0 pods still starting: "Utilization 200% against 50%. Starting pods count as 0% to damp the change: ratio 2.00 × 8 pods = 16.00, rounded up to 16."
With starting pods burning their full CPU limit, the spike scenario still peaks at 20 pods and catches up after 80 seconds, exactly as without the burn, because none of those readings are used. AWS takes a similar approach: an EC2 instance is not counted in the group's aggregated metrics until its warmup time has passed.
Stabilization and scaling policies
The raw recommendation is smoothed before it is applied. A stabilization window makes the HPA look back over its recent recommendations: when scaling down it uses the highest one in the window, and when scaling up the lowest. The defaults are 300 seconds for scaling down and none for scaling up, so the HPA adds pods at the next check but removes them only after load has stayed low for five minutes. This prevents flapping, where pods are removed during a short dip and then added back.
Scaling policies then limit how fast the count may change. Each policy allows a number of pods or a percentage of the current count per period, and by default the most permissive policy wins. The default scale-up policies allow doubling or adding four pods every 15 seconds, whichever is more; the default scale-down policy allows removing all excess pods at once. Both can be changed per HPA with the behavior field, which became stable in Kubernetes 1.23.
The documentation's example of a gentler scale-down allows 4 pods or 10% per minute. Run through the simulator's controller, 80 replicas wanting 10 go 80 → 72 → 64 → 57 → 51 → 45 → 40 → 36 → 32 → 28 → 24 → 20, one step per minute: 10% of the count while that is more than 4, then 4 at a time.
Why autoscaling lags
From the moment load rises, several delays add up before new capacity serves it: up to 15 seconds for the metrics window to reflect the change, up to 15 seconds until the next HPA check, the rate limit on how many pods each check may add, and the time each new pod takes to be scheduled, start and pass its readiness probe. In clusters without spare nodes, the node autoscaler adds a further delay to bring up machines. The table shows the fivefold spike in the simulator with three start-up times.
| Pod start-up | Seconds to ready | Capacity caught up after | Demand not served |
|---|---|---|---|
| Fast | 15 | 40 s | 1.0% |
| Typical | 55 | 80 s | 2.2% |
| Slow | 180 | 205 s | 10.5% |
"Demand not served" is the share of all CPU the traffic needed over 25 minutes that ready pods could not supply. Start-up time dominates, which is why fast-starting images, readiness probes that pass as soon as the app can serve, and some spare capacity (a lower target, or a higher minimum) matter as much as the autoscaler's settings. Predictable peaks are better handled by scaling ahead of time, on a schedule or with predictive scaling.
Other autoscalers
| Autoscaler | Scales | How it decides |
|---|---|---|
| Kubernetes HPA | Pods | Ratio of a metric to its target, every 15 s, with stabilization and rate limits |
| Kubernetes VPA | Pod CPU and memory requests | Recommends requests from observed usage; can apply them by recreating pods |
| KEDA | Pods, including to and from zero | Event sources such as queue length or stream lag, feeding an HPA |
| Cluster Autoscaler, Karpenter | Nodes | Add nodes when pods cannot be scheduled; remove underused ones |
| EC2 Auto Scaling, target tracking | Instances | Keeps a metric near a target using CloudWatch alarms it manages; instances in warmup are left out of the metric; scales in more gradually than out |
| EC2 Auto Scaling, step scaling | Instances | Fixed adjustments for each band of an alarm's metric |
| Predictive scaling | Instances | Forecasts load from past daily and weekly patterns and adds capacity before it is needed |
Choosing a metric
A metric works for this kind of proportional control if it falls in proportion as copies are added. Average CPU utilization usually does, for services where CPU is the bottleneck. Requests per second per pod works for many web services and responds faster to load than CPU does. AWS's documentation gives examples that do not work for target tracking: the total request count at the load balancer, which does not change when instances are added, and latency, which rises with load but not proportionally.
For workers that consume a queue, the backlog per worker is a better signal than CPU, which is what tools like KEDA scale on. Memory is rarely a good scaling metric: many applications do not release memory when load falls, so the autoscaler scales up and never back down.
The target sets the headroom. A 50% CPU target means each pod could absorb a doubling of load before it saturates, enough to ride out the lag above; 90% is cheaper but leaves almost nothing for a spike.
History
Amazon EC2 Auto Scaling launched in 2009, together with CloudWatch and Elastic Load Balancing, and scaled groups of instances on alarms. Target tracking policies, which pick the adjustments themselves, followed in July 2017. Kubernetes added the Horizontal Pod Autoscaler in version 1.1 in November 2015, scaling on CPU; the autoscaling/v2 API, with custom and external metrics and configurable scaling behavior, became stable in Kubernetes 1.23.
Autoscaling questions and answers
How does the Kubernetes HPA calculate the number of replicas?
Every 15 seconds it averages the metric across the pods that have usable readings and applies desiredReplicas = ceil(currentReplicas × currentMetric / targetMetric). For CPU, the metric is usage as a percentage of each pod’s CPU request. If the ratio is within 10% of 1.0 it does nothing. The result is then smoothed by a stabilization window and limited by scaling policies before it is applied.
Why does the HPA scale down so slowly?
By default it uses a 300-second scale-down stabilization window: it scales to the highest recommendation it made in the last five minutes. That stops it removing pods during a brief dip and then having to add them back. Scale-up has no window by default, so it reacts at the next 15-second sync.
Why does autoscaling lag behind a traffic spike?
Several delays add up: metrics cover the last 15 seconds, the controller checks every 15 seconds, scale-up is limited to doubling or adding 4 pods per 15 seconds, and each new pod must be scheduled, start and pass its readiness probe. In this simulator, with pods that take 55 seconds to become ready, capacity catches up with a fivefold spike after 80 seconds.
Does the HPA count CPU from pods that are still starting?
No. For CPU, a pod that is not yet ready, or whose latest sample was taken before it became ready, is set aside for the first five minutes after it starts (the CPU initialization period). When scaling up, those pods are counted as using 0% so they damp the next step rather than inflate it. This stops a burst of startup CPU, such as a JVM warming up, from triggering more scaling.
What happens if a pod has no CPU request?
CPU utilization is defined as a percentage of the request, so if any container in the pods lacks a CPU request, utilization is undefined and the HPA takes no action on that metric.
What is the difference between the HPA, VPA and Cluster Autoscaler?
The Horizontal Pod Autoscaler changes the number of pods. The Vertical Pod Autoscaler changes the CPU and memory requests of each pod. The Cluster Autoscaler (or Karpenter) adds and removes nodes so that pods have somewhere to run. They are often used together: the HPA adds pods, and if they do not fit, the node autoscaler adds machines.
How is AWS target tracking different from the Kubernetes HPA?
Both aim to keep a metric near a target. EC2 Auto Scaling target tracking manages CloudWatch alarms for you, scales instances rather than pods, leaves new instances out of the aggregated metric until their warmup time has passed, and scales in more gradually than it scales out. AWS also offers step scaling, with fixed adjustments per alarm band, and predictive scaling based on past load patterns.
Further reading
- Kubernetes documentationHorizontal Pod AutoscalingThe algorithm, readiness handling, scaling behavior and defaults.
- Kubernetes sourcepkg/controller/podautoscalerThe controller this simulator ports: replica_calculator.go and horizontal.go.
- AWS documentationTarget tracking scaling policies for Amazon EC2 Auto ScalingHow target tracking, warmup and metric choice work on EC2.
- KEDA documentationKEDA conceptsEvent-driven autoscaling on queues and streams, including scaling to zero.