Karpenter vs Cluster Autoscaler: Which One Should Be Scaling Your Nodes?
September 1, 2026 · 12 min read · by Harshit Luthra
Cluster Autoscaler scales node groups you defined in advance. Karpenter provisions individual instances on demand from a wide pool and continuously consolidates them. If your waste comes from oversized or fragmented node groups, Karpenter usually wins. If you are not on AWS or you need strict, predictable node shapes, Cluster Autoscaler is still the right tool.
The short answer
Both tools do the same job — add nodes when pods cannot be scheduled, remove nodes when they are not needed — and they do it in fundamentally different ways.
Cluster Autoscaler works through node groups: Auto Scaling Groups on AWS, managed instance groups on GCP, scale sets on Azure. You define the groups and their instance types in advance. When pods are pending, it simulates which group would fit them and increases that group’s desired count by some number.
Karpenter skips the group abstraction. When pods are pending, it looks at what they actually need — CPU, memory, architecture, zone, GPU, taints — and launches instances that fit, chosen from a broad set of instance types you allow. Then it keeps watching, and when the same pods would fit on cheaper or fewer nodes, it replaces them.
The practical difference is granularity. Cluster Autoscaler picks from a menu you wrote. Karpenter computes an order.
Node groups are where the waste comes from
Almost every cluster I have looked at for cost reasons had the same structure: three or four node groups, each pinned to one or two instance types, sized by someone’s estimate a year earlier and never revisited. Waste accumulates in three predictable ways.
Shape mismatch. Your workloads are memory-heavy, your node group is a general-purpose instance type, and you run out of memory with half the CPU idle on every node. You are paying for cores nobody schedules onto.
Fragmentation. Scale-up added a node for three pending pods; the node has room for twelve. Scale-down will not remove it because the utilization threshold has not been crossed, or because one unmovable pod landed on it. Multiply by a few dozen nodes.
Ratchet effect. Node groups get bigger after incidents and never get smaller, exactly the way memory limits do after an OOMKill. On the AWS bill reduction engagement the compute line was about 60% of spend and a lot of it was sitting idle for precisely this reason.
Cluster Autoscaler can only pick a group; it cannot fix the fact that no group is the right shape. Karpenter can pick an instance, so it can.
What each one actually gives you
| Cluster Autoscaler | Karpenter | |
|---|---|---|
| Unit of scaling | Node group (ASG / MIG / VMSS) | Individual instance |
| Instance selection | You pre-define the groups | Chosen per pending pod set from an allowed list |
| Scale-up latency | ASG API, then boot — often minutes | Direct fleet call, then boot — typically faster |
| Scale-down | Node below a utilization threshold for a set period | Continuous consolidation, including replacing a node with a cheaper one |
| Bin-packing quality | Bounded by group shapes | Bounded by the instance families you allow |
| Spot handling | Per-group, with a separate spot group | Diversified across many types in one NodePool |
| Cloud support | AWS, GCP, Azure and more | AWS mature; Azure provider; others vary |
| Node churn | Low; nodes are long-lived | Higher by design; consolidation and expiry rotate nodes |
| Config surface | Flags plus your existing ASGs and IaC | NodePool and EC2NodeClass CRDs in the cluster |
| Operational risk | Well understood, boring | Newer, and it moves your pods more often |
Consolidation is the feature people actually come for
Scale-down in Cluster Autoscaler is one-directional: a node is either unneeded and removed, or it stays. The default is to consider a node for removal once it has been below --scale-down-utilization-threshold (0.5 by default) for --scale-down-unneeded-time (10 minutes by default). A node sitting at 55% utilization forever is simply never reclaimed.
Karpenter’s consolidation asks a different question: given everything running right now, is there a cheaper arrangement? That includes deleting an empty node, deleting an underutilized node by moving its pods elsewhere, and — the one that has no Cluster Autoscaler equivalent — replacing a node with a smaller or cheaper one.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: "10%" # bound the blast radius of a consolidation pass
- nodes: "0" # freeze churn during business hours
schedule: "0 9 * * mon-fri"
duration: 8h
template:
spec:
expireAfter: 720h # rotate nodes every 30 days for patching
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
- 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"]
Two things in that manifest matter more than the rest. budgets is what keeps consolidation from becoming a self-inflicted rolling outage — without it, a single pass can move far more workload at once than you intended. And leaving requirements broad is the entire point: constrain to instance families and generations you trust, then let Karpenter choose within them. A NodePool pinned to two instance types is Cluster Autoscaler with extra CRDs.
expireAfter is quietly one of the best reasons to adopt Karpenter even if cost is not your driver. Nodes that rotate every 30 days are nodes that get patched without anyone scheduling a patching project.
Spot capacity: two different stories
With Cluster Autoscaler, spot means a separate node group with a mixed instances policy, and the diversification you get is whatever you configured in that ASG. When capacity for those types is reclaimed, you fall back to whatever the group can still get.
With Karpenter, spot diversification is a consequence of leaving requirements wide. It evaluates across the allowed instance types and picks from pools with better availability, which materially reduces how often you get interrupted. On AWS you need to wire up interruption handling so a two-minute reclaim notice results in a graceful drain rather than a hard stop:
# Karpenter watches an SQS queue fed by EventBridge rules for spot interruption,
# instance rebalance, and scheduled-change events, then cordons and drains early.
settings:
interruptionQueue: karpenter-interruption-queue
Skip that and spot on Karpenter is worse than spot on Cluster Autoscaler, because you have more spot instances and no early warning on any of them.
Either way, spot belongs on workloads that can lose a node without a customer noticing: stateless services with several replicas, batch, CI runners, dev and staging. It does not belong under a stateful primary, and no autoscaler will save you from that decision.
The honest cost of Karpenter
I have migrated clusters to Karpenter and I would do it again, but the pitch usually skips the parts that generate tickets.
Node churn goes up, on purpose. Consolidation and expiry both move pods. Every workload without a PodDisruptionBudget will be restarted more often than it used to be, and you will find out which ones handle SIGTERM badly. That discovery is valuable and it is also a week of your life.
Instance-type assumptions break. DaemonSets whose resource requests were tuned for one node size become either wasteful or unschedulable across a wide instance range. Node selectors written against specific instance-type labels stop matching. Anything licensed per node gets more interesting.
Zonal storage gets sharper edges. A pod with an EBS-backed PVC in one availability zone can only be rescheduled into that zone. Karpenter understands this, but a consolidation pass that cannot find room in the right zone leaves you with a node that will not go away, and it takes a moment to work out why.
It is another controller with cluster-wide power. It creates and terminates instances. Its IAM role, its CRDs, and its upgrade path are now part of your platform’s blast radius. The v1 API stabilised the CRD shape, but earlier clusters carry a Provisioner-to-NodePool migration in their history.
None of these are reasons not to adopt it. They are reasons to adopt it deliberately rather than during an incident.
Where Cluster Autoscaler is still the right answer
- You are not on AWS. On GKE, node auto-provisioning covers much of the same ground natively. On smaller clouds and on-prem, Cluster Autoscaler is the option that exists.
- You need predictable node shapes. Compliance, licensing, hardware affinity, or a workload tuned to one instance type.
- Your cluster is small and stable. Below a handful of nodes, consolidation has almost nothing to consolidate and you have added a controller for no return.
- Nobody owns the platform. Karpenter rewards a team that watches it. Cluster Autoscaler tolerates being ignored, which is sometimes the honest constraint.
Migrating without a big bang
The safe path is to run both, with clearly disjoint ownership, and move workloads across in slices. This is the same discipline as any zero-downtime Kubernetes migration: keep the old path working, shift a fraction, watch, repeat.
- Install Karpenter alongside Cluster Autoscaler. Scope Cluster Autoscaler to explicitly tagged ASGs with
--node-group-auto-discoveryso it cannot claim Karpenter’s nodes. - Create one NodePool with broad requirements, on-demand only to start, and consolidation set to
WhenEmpty. Least aggressive setting that still does something. - Move one non-critical workload by adding a node selector or affinity that only Karpenter nodes satisfy. Watch scheduling latency and cost for a week.
- Add PodDisruptionBudgets everywhere before you turn on
WhenEmptyOrUnderutilized. This is the step teams skip and then blame Karpenter for. - Turn on underutilized consolidation with a conservative
budgetsblock and a business-hours freeze. - Introduce spot on the workloads that tolerate it, with interruption handling wired up first.
- Shrink the old node groups to zero over several weeks. Keep them defined but empty for a while — an empty ASG is a cheap rollback.
Only after all of that is it worth deleting Cluster Autoscaler.
How to decide in five minutes
Ask three questions.
Is my waste in node shape and fragmentation, or in workload requests? Look at node-level CPU and memory allocation versus actual usage. If nodes are 40% allocated, Karpenter helps. If nodes are 95% allocated with requests that are three times real usage, your problem is rightsizing, not the autoscaler, and you should fix that first — the Kubernetes cost optimization guide covers that side, and OOMKilled debugging covers why those requests got inflated in the first place.
Am I on AWS with a team that owns the platform? If yes, Karpenter. If no, Cluster Autoscaler.
Do my workloads have PodDisruptionBudgets and handle SIGTERM correctly? If not, fix that before you adopt anything that moves pods for a living. You will need it either way.
Getting this wrong in either direction is expensive: an autoscaler that is too timid burns money quietly, and one that is too aggressive burns availability loudly. If you want the analysis done against your actual cluster and bill, that is cloud cost optimization work, and the migration itself is DevOps and platform engineering.
Written by Harshit Luthra, an independent infrastructure and AI engineering consultant. Stuck on something similar? →
related
If this is live for you right now
Cloud Cost Optimization (FinOps)
Your AWS or GCP bill keeps climbing and nobody knows exactly why. I find the waste and cut it, and your system stays just as fast and reliable as before.
ServiceDevOps & Platform Engineering
Kubernetes set up properly, infrastructure in code, and CI/CD that deploys without drama. The platform your team wishes they already had.
~42% lower monthly AWS spend in 3 weeksCut a SaaS startup's AWS bill by ~42%
A Series-A SaaS team's AWS bill had tripled in a year and nobody could say why. A focused FinOps pass cut it ~42% without touching reliability.
Full cutover with zero customer-facing downtimeZero-downtime migration to Kubernetes with multi-cloud ingress
A team moving from hand-managed VMs to Kubernetes needed it done without an outage. A staged, GitOps-driven migration with weighted ingress shifted traffic gradually and reversibly, with zero downtime.
Questions people ask about this
Is Karpenter always cheaper than Cluster Autoscaler?+
No. Karpenter tends to be cheaper when your waste comes from coarse node groups — a few fixed instance types that never quite fit the workload shape, plus nodes left half-empty after scale-down. If your node groups are already well matched to your workloads and your bin-packing is tight, Karpenter's advantage shrinks to consolidation alone, and you are paying operational cost for a smaller return.
Can I run Karpenter and Cluster Autoscaler at the same time?+
Yes, and during a migration you usually should, as long as they manage disjoint sets of nodes. Karpenter manages the nodes it provisions; Cluster Autoscaler manages its own node groups. The failure mode to avoid is both controllers believing they own the same capacity, so keep Cluster Autoscaler scoped to explicitly tagged ASGs and let Karpenter handle everything else.
Does Karpenter work outside AWS?+
AWS is where it is most mature and most widely run in production. There is an Azure provider, and other providers exist in varying states of maturity. On GKE, node auto-provisioning already covers a lot of the same ground natively. If you are multi-cloud or not on AWS, Cluster Autoscaler remains the safer default because it supports every major cloud's node group abstraction.
What breaks when you switch to Karpenter?+
Anything that assumed stable node identity or a fixed instance type. DaemonSets sized for one instance shape, node-local caches, PVs bound to a zone, licence-per-node software, and node selectors written against instance-type labels. Karpenter also churns nodes deliberately through consolidation and expiry, so workloads without PodDisruptionBudgets get moved more often than they used to be.
How do I stop Karpenter from disrupting a critical pod?+
Set a PodDisruptionBudget, and for workloads that must not move at all, add the `karpenter.sh/do-not-disrupt: "true"` annotation to the pod. Use disruption budgets on the NodePool to bound how much churn can happen at once, and give batch jobs the same do-not-disrupt annotation so a consolidation pass does not kill a long-running job halfway through.