Cluster Autoscaler
Summary
- The Cluster Autoscaler (CA) adds nodes when pods cannot be scheduled and removes nodes that sit underused.
- Insight: The CA is driven entirely by the scheduler. A pod stuck
Pendingfor lack of capacity is its trigger to grow. - Key Concepts: node groups,
Pendingpods, scale-down utilization threshold, safe-to-evict,PodDisruptionBudget. - When to Use: Any cluster where pod scaling can outrun fixed node capacity, which is nearly every production cluster.
- Limitations: Node provisioning takes minutes, scale-down is conservative, and pods without requests confuse its bin-packing math.
Recipe
Run the Cluster Autoscaler for your cloud, or adopt Karpenter for provisioner-based scaling.
Define node groups with a minimum and maximum size the CA is allowed to move between.
Set accurate resource requests so the CA can compute whether a pod fits.
Protect critical workloads with PodDisruptionBudgets so scale-down cannot evict them unsafely.
Confirm the loop by creating unschedulable pods and watching a node appear.
Working Example
A Deployment sized so extra replicas force a new node:
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-worker
spec:
replicas: 12
selector:
matchLabels:
app: batch-worker
template:
metadata:
labels:
app: batch-worker
spec:
containers:
- name: worker
image: registry.example.com/worker@sha256:abc123
resources:
requests:
cpu: "1"
memory: 2GiWatch the scale-up loop:
kubectl get pods -l app=batch-worker -o wide
kubectl get pods --field-selector=status.phase=Pending
kubectl get nodes --watchInspect the CA's own decisions through its status ConfigMap:
kubectl -n kube-system describe configmap cluster-autoscaler-statusAnnotate a pod that must never block a node from draining:
metadata:
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"Deep Dive
Scale-up logic
Every scan, the CA looks for Pending pods that failed to schedule because no node had room.
It then simulates adding a node from each eligible node group and picks one where the pending pods would fit. It respects nodeSelector, affinity, taints, and topology constraints during that simulation.
Only requests matter here. A pod requesting 1 CPU needs a node that can seat that request even if the app uses far less.
Scale-down logic
The CA marks a node as removable when its utilization stays below a threshold (10% by default) and its pods can move elsewhere.
It drains the node, honoring PodDisruptionBudgets, then removes it from the node group. Nodes with pods that block eviction stay up.
Scale-down is deliberately slow and unforced to avoid cutting capacity right before demand returns.
What blocks scale-down
A node will not be removed if it hosts pods with no controller, pods using local storage, pods with restrictive PDBs, or pods annotated safe-to-evict: "false".
kube-system pods and DaemonSet pods are handled specially so they do not pin nodes indefinitely.
Cluster Autoscaler versus Karpenter
The classic CA scales fixed-shape node groups up and down. You predefine instance types per group.
Karpenter provisions right-sized nodes directly from pending pods, choosing instance types on the fly and consolidating aggressively. It often improves both packing and cost on AWS.
Expander strategies
When several node groups could satisfy the pending pods, the CA uses an expander to choose one.
Common strategies are least-waste, which picks the group that leaves the least idle capacity, and priority, which follows an explicit ranking you define.
Choosing least-waste usually gives better bin-packing, while priority lets you steer toward cheaper or spot-backed groups first.
Gotchas
Pods without resource requests break bin-packing. The CA cannot reason about a pod that claims zero capacity, so it may not add the node you need.
Node provisioning latency is real. A new node can take one to several minutes to join, so latency-sensitive spikes need headroom, not just reactive scaling.
Scale-down surprises teams that omit PDBs. A drain can remove several replicas at once and briefly starve a service.
Local storage pins nodes. A pod using emptyDir or hostPath blocks scale-down unless explicitly marked safe to evict.
Mismatched node group limits stall scaling. If maxSize is already reached, pods stay Pending no matter the demand.
Overly large requests waste money. If requests far exceed real usage, the CA provisions nodes you do not need.
Alternatives
Karpenter replaces node groups with just-in-time provisioning and stronger consolidation. Prefer it on AWS for cost and packing.
Managed autoscaling from a cloud provider wraps the CA with node pool defaults. Convenient, but you inherit its scan and cool-down tuning.
Over-provisioning pods run low-priority pause pods that reserve headroom. They get evicted instantly when real work arrives, hiding node latency.
Static capacity simply runs a fixed fleet. It is the right call for steady, predictable load where scaling churn adds risk without savings.
FAQs
Why is my pod Pending even with the CA running?
Check the node group maxSize, the pod's requests, and any affinity or taint that no node can satisfy.
How fast does a node appear? Expect one to several minutes depending on the cloud and image pull time. Plan headroom for faster spikes.
Does scale-down respect PodDisruptionBudgets? Yes. The drain honors PDBs, which is exactly why critical workloads should define them.
Can the CA remove the last node in a group?
It respects each group's minSize, so it will not shrink below the minimum you set.
Should I use Karpenter or the classic CA? On AWS, Karpenter usually packs better and cuts cost. Elsewhere the classic CA with tuned node groups is the standard choice.
Why did a node stay up despite low usage?
Something on it blocks eviction: an unmanaged pod, local storage, a strict PDB, or a safe-to-evict: "false" annotation.
Related
- The Three Axes of Kubernetes Scaling
- Autoscaling Basics
- Horizontal Pod Autoscaler
- Vertical Pod Autoscaler
- KEDA Event-Driven Scaling
- Autoscaling Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).