Kubernetes Autoscaling in Practice: HPA, VPA, and Cluster Autoscaler

Three scaling loops, not one feature
Kubernetes does not have an autoscaler. It has three, they act on different objects at different speeds, and most autoscaling incidents come from treating them as one thing — or letting two of them fight over the same workload. Before tuning anything, hold the mental model clearly:
- Horizontal Pod Autoscaler (
HPA) changes the number of pod replicas for a workload, reacting to observed load in seconds to minutes. - Vertical Pod Autoscaler (
VPA) changes the CPU and memoryrequestsandlimitson each pod, right-sizing a single replica rather than adding more. - Cluster Autoscaler (or
Karpenter) changes the number of nodes so there is somewhere for new pods to land, reacting to unschedulable pods.
HPA scales the app, VPA sizes the app, and the node autoscaler makes room for
the app. All three depend on one thing being correct: accurate resource
requests. Get requests wrong and every loop above them makes bad decisions with
confidence.
HPA: scaling out the replicas
The Horizontal Pod Autoscaler is the workhorse and the one most teams reach for
first. It watches a metric, compares it to a target, and adjusts replica count to
close the gap. The default metric is CPU utilization as a percentage of the pod's
CPU request — which is exactly why requests matter. If a pod requests 250m and
HPA targets 60% CPU, it scales when average usage crosses 150m, not when the
node is busy.
Key practicalities:
- You need a metrics source. CPU and memory targets require
metrics-serverrunning in the cluster. Without it,kubectl get hpashows<unknown>and the controller does nothing. - CPU is a poor proxy for many workloads. Queue depth, requests per second,
or p95 latency often describe load far better. Custom and external metrics let
HPAscale on those, andKEDAextends this to event sources like queue length, letting a consumer scale — including to zero — off the backlog it is actually draining. - Tune the behavior, not just the target. Aggressive scale-up with slow, stabilized scale-down is usually right: add capacity fast when load arrives, remove it cautiously so a brief dip does not trigger thrashing.
HPA is the correct default for stateless, horizontally scalable services behind
a load balancer. It does nothing useful for a workload that cannot run more than
one copy.
VPA: right-sizing each pod
The Vertical Pod Autoscaler solves a different problem: not how many replicas,
but how big each one should be. It observes actual consumption over time and
recommends — or automatically applies — better requests and limits. This is
the antidote to the two most common cluster-economics failures: every pod
requesting far more than it uses (wasted, unschedulable capacity) or requesting
too little (OOMKilled pods and CPU throttling).
Two things to understand before enabling it:
- Applying a change means restarting the pod. Kubernetes cannot resize a
running pod's requests in place in most setups, so
VPAinAutomode evicts and recreates pods to apply new sizing. Respect aPodDisruptionBudgetand use it only where restarts are safe. VPAandHPAconflict on the same metric. If both act on CPU or memory for one workload, they contradict each other —HPAadds replicas to lower per-pod CPU whileVPAshrinks per-pod CPU requests, and they oscillate. Run them together only whenHPAscales on a custom metric (like requests per second) andVPAhandles memory. Many teams instead runVPAin recommendation-only mode and feed its numbers into their manifests.
Cluster Autoscaler and Karpenter: scaling the nodes
Pods only run if a node has room. When HPA or a deployment creates pods that
cannot be scheduled, the node autoscaler adds capacity; when nodes sit
underutilized, it removes them. Two approaches dominate:
- Cluster Autoscaler works through node groups (managed instance groups / node pools). You predefine the instance types, and it grows and shrinks those groups to fit pending pods. Predictable, but you own the shape of the groups.
Karpenterprovisions nodes just-in-time from a broad set of instance types, picking the cheapest shape that fits the pending pods and consolidating workloads onto fewer nodes as demand falls. It reacts faster and bin-packs better, at the cost of a newer, more dynamic model to reason about.
Whichever you use, the interaction with your compute strategy matters. Diversify
across instance types so the autoscaler is not stranded when one t3.large pool
runs dry, and place scaling-hungry workloads on infrastructure sized for them —
often the dense
high-performance servers that give the
scheduler the most room to pack pods.
Making the three loops cooperate
The loops are independent controllers with no shared brain, so cooperation is something you design, not something you get for free.
- Set honest
requestsfirst. Every loop keys off them. Measure real usage, then set requests near the steady-state working set — not the peak, not a round guess. This single input does more for autoscaling quality than any tuning parameter. - Give the node autoscaler slack. If pods scale up but nodes take minutes to join, users feel the gap. Over-provisioning with low-priority "pause" pods reserves warm headroom that real workloads preempt instantly, trading a little cost for fast scale-out.
- Protect availability during scale-down. A
PodDisruptionBudgetstops the cluster autoscaler andVPAfrom draining so many replicas at once that the service dips below its floor. - Do not double-scale one signal. Pick one loop per metric per workload. Overlapping controllers on the same signal is the root cause of most autoscaling oscillation.
Common failure modes
- Missing
metrics-server.HPAsilently does nothing. Checkkubectl get hpafor<unknown>targets. - No
limits, norequests. Pods with unset requests are invisible to the scheduler's math; the autoscalers cannot reason about capacity they cannot see. - Scaling on CPU when the bottleneck is elsewhere. A memory-bound or
I/O-bound service will
OOMKillwhile CPU sits low andHPAstays idle. Scale on the metric that actually saturates. - Nodes that cannot shrink. A single pod without a disruption budget, or local storage, can pin an expensive node open indefinitely. Audit what blocks consolidation.
- Cold-start stampedes. Scaling from too few replicas means new pods take traffic before they are warm. Set sensible minimums and readiness probes.
Where to start
Begin with observability, not automation. Turn on metrics-server, measure a
week of real CPU and memory per workload, and correct your requests — most
clusters are badly mis-sized, and fixing that alone recovers meaningful capacity.
Then add HPA to stateless services on a metric that reflects real load, enable
the node autoscaler with a little warm headroom, and use VPA in
recommendation mode to keep sizing honest over time. Treat the three loops as one
system with one shared input, and validate behavior against a load test before
production traffic does it for you.
Autoscaling that works quietly is the product of deliberate design across scheduling, cloud infrastructure, and the metrics that drive it. If you want a review of how your clusters scale under real load, or a Kubernetes platform built to right-size itself, talk to our team.


