The short answer
On most EKS clusters, Karpenter cuts node cost more than Cluster Autoscaler — usually 20–40% on the compute line — because it picks the cheapest instance type that fits your pending pods instead of scaling a fixed node group, and it consolidates underused nodes automatically. Cluster Autoscaler (CA) is simpler, cloud-agnostic, and fine when your workloads are uniform. If your pods have mixed CPU/memory shapes and you're on AWS, Karpenter almost always wins on the bill.
Both solve node autoscaling — adding and removing EC2 instances. That's different from pod autoscaling (HPA/VPA/KEDA), which changes replica count or pod size. You usually run both layers together: HPA adds pods, the node autoscaler adds machines to place them on. If you're fuzzy on the pod layer, start with HPA, VPA, and KEDA explained, then come back here for the node layer.
How each one actually decides
Cluster Autoscaler: node groups in, node groups out
CA watches for pods stuck in Pending because no node has room (FailedScheduling). When it sees them, it increases the desired count of a matching Auto Scaling Group (ASG). The instance type is fixed by whoever defined that node group.
# A managed node group Cluster Autoscaler can scale (Terraform-ish)
node_groups:
general:
instance_types: ["m6i.xlarge"] # every node is this shape
min_size: 2
max_size: 20
labels: { workload: general }
tags:
"k8s.io/cluster-autoscaler/enabled": "true"
"k8s.io/cluster-autoscaler/my-cluster": "owned"
The consequence: if a pod needs 500m CPU and 6Gi memory, CA still boots a whole m6i.xlarge (4 vCPU / 16Gi). The extra capacity sits idle until other pods land on it. To get instance-shape diversity you create more node groups — one per shape — and CA picks between them with an "expander" (least-waste, priority, random). It works, but you're hand-maintaining the menu of instances.
Karpenter: provision the right node, just-in-time
Karpenter skips ASGs. It reads the actual resource requests of pending pods and calls the EC2 Fleet API directly to launch the cheapest instance that fits, from a broad pool you allow. You describe constraints, not a fixed shape.
# Karpenter v1 NodePool — the allowed universe of nodes
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"] # let it pick Graviton when cheaper
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"] # modern gens only
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidationAfter: 30s
limits:
cpu: "1000" # hard ceiling on the pool
Given a wide requirements block, Karpenter might satisfy the same pending pods with a c7g.large spot node at a fraction of an on-demand m6i.xlarge. You're handing it a budget of shapes and letting it optimize inside your guardrails.
Where the cost difference comes from
Three mechanisms drive the gap:
1. Bin-packing accuracy. CA rounds every scale-up to a pre-defined shape. Karpenter sizes the node to the workload. If ten pods each want 1.5Gi, Karpenter can launch one node that holds exactly them; CA launches whatever the ASG says and eats the slack.
2. Consolidation. This is the big one. Karpenter continuously asks "could these pods fit on fewer/cheaper nodes?" and, if yes, drains and replaces them. Over a day of traffic peaks and troughs, that reclaims the idle capacity CA leaves behind. CA only removes a node when it's been almost entirely empty for ~10 minutes — it never repacks.
3. Spot + architecture diversity. Karpenter can span dozens of instance types and both x86 and Graviton in one pool, which both lowers spot interruption risk and lets it grab the cheapest offering per launch. Matching that on CA means many ASGs to maintain.
A worked example. Say steady state is 40 vCPU and 150Gi of requests across mixed pods:
| Approach | Nodes | Monthly compute (us-east-1, rough) |
|---|---|---|
CA, single m6i.xlarge group, on-demand | ~13 × m6i.xlarge | ~$1,990 |
| Karpenter, mixed c/m/r, on-demand + consolidation | right-sized set | ~$1,450 |
| Karpenter, spot-first for stateless tier | mostly spot | ~$650 |
Numbers vary with your workload, but the shape holds: bin-packing gets you the first cut, spot gets you the second. For the broader picture beyond nodes, see FinOps for Kubernetes: practical cost optimization and the cluster-wide Kubernetes cost optimization guide.
Speed matters too
CA's loop involves scaling an ASG, which then launches an instance — typically 60–90 seconds before a node is Ready. Karpenter calls Fleet directly and skips the ASG hop, usually landing a node in 30–45 seconds. During a traffic spike, that difference is the gap between a brief queue and visible 503s. Faster provisioning also means you can run tighter — less standby headroom — which is itself a cost win.
The right-request prerequisite
Neither autoscaler can bin-pack well if your pods lie about what they need. A pod with no CPU/memory requests is invisible to the scheduler's math; a pod that requests 2Gi but uses 200Mi forces both tools to reserve capacity that's never touched. Before you A/B the autoscalers, fix requests:
# Find the worst over-requesters (requested vs actual)
kubectl top pods -A --sum=false | sort -k3 -h | tail -20
Run VPA in recommendation mode to get honest numbers, then set requests close to the p95 of real usage. This single step often saves more than switching autoscalers — and it makes the switch pay off harder.
Failure modes to plan for
-
Pending pods that never schedule. Both tools leave pods
Pendingif no allowed instance can host them — for example a pod requesting a GPU when your pool only allowsc/m/r. Karpenter's wide requirements make this less common, but a too-narrownodeSelectoror an un-tolerated taint will still strand pods. Debugging that path is exactly the Pod stuck in Pending / FailedScheduling workflow. -
Consolidation churn. Aggressive
consolidationAftercan evict and reschedule pods often. Protect critical workloads with PodDisruptionBudgets and setdo-not-disruptannotations on pods that must not move:metadata: annotations: karpenter.sh/do-not-disrupt: "true" -
Spot interruptions. Spot nodes can vanish with a 2-minute warning. Keep stateful and singleton workloads on
on-demand, spread stateless replicas across many instance types, and make sure readiness probes are correct so replacement pods only take traffic when truly ready — see liveness, readiness, and startup probes.
Decision framework
| Your situation | Pick |
|---|---|
| On AWS/EKS, mixed pod shapes, want lowest cost | Karpenter |
| Heavy spot usage across many instance types | Karpenter |
| Need node provisioning under ~45s for spiky traffic | Karpenter |
| Multi-cloud or on GKE/AKS where CA is the native path | Cluster Autoscaler |
| Uniform workloads, few node shapes, want minimal moving parts | Cluster Autoscaler |
| Strict, audited node images with one blessed instance type | Cluster Autoscaler |
Karpenter is AWS-first (with a maturing Azure provider); Cluster Autoscaler runs everywhere and is the default on GKE and AKS. On EKS in 2026, Karpenter is the cost-optimizer's default, and CA is the conservative, portable choice.
Migrating without a cliff
You don't have to flip everything at once. Run both side by side: keep CA on your existing node groups, install Karpenter with a NodePool that has a limits.cpu ceiling, and cordon the CA groups gradually so new pods land on Karpenter nodes. Watch cost and pending-pod counts for a week before removing the old ASGs. Cap Karpenter with limits so a runaway workload can't provision an unbounded (and unbudgeted) fleet.
Bottom line
Cluster Autoscaler answers "add another node like the ones I defined." Karpenter answers "add the cheapest node that fits what's actually pending, and repack when you can." For heterogeneous EKS workloads that second question is worth 20–40% off your compute line — but only after your pod requests are honest. Fix requests first, then let Karpenter's bin-packing and consolidation do the rest. For the layers above the node, pair this with pod-level autoscaling and the wider AWS cost optimization playbook.