Most comparisons treat these as competing implementations of the same idea. They are not. They operate on different objects.
Cluster Autoscaler manages node groups. Karpenter manages nodes.
Cluster Autoscaler's unit of work is a group you defined in advance — an EC2 Auto Scaling group, an AKS VM scale set, a GCE managed instance group. It cannot create one, change its shape, or decide what instance type belongs in it. Its only real action is changing an integer: the group's desired capacity.
Karpenter has no intermediary. It reads pending pods, computes instance types that satisfy them, and launches directly through the cloud provider's fleet API. Each node is an independent object with its own lifecycle, described by a NodeClaim.
Nearly every practical difference — latency, Spot economics, bin-packing, upgrades, and the specific ways each fails — falls out of that one distinction.
How each one actually works
Cluster Autoscaler: a control loop over node groups
Every --scan-interval (default 10s), CA lists pods the scheduler failed to place, builds a template node for each node group representing what a new member would look like, runs the scheduler's predicates against the pending pods, picks between viable groups using an expander, then increments that group's desired capacity and waits up to --max-node-provision-time (default 15m) for a node to register.
The documented default expander is least-waste, choosing the group left with the least idle CPU; the others are random, most-pods, least-nodes, price (GCE, GKE and Equinix Metal only), priority and grpc. Distributions often override the default in their charts.
The template node is the design constraint. CA assumes, in its own words, that "inside a node group, all machines have identical capacity and have the same set of assigned labels." Attach a mixed instance policy with differently-shaped types and CA simulates against the *first* type listed: larger types after it waste capacity, smaller ones mean pods CA predicted would fit do not.
Scale-down is stricter and separate. A node goes only when all hold: CPU and memory *requests* on it are below --scale-down-utilization-threshold (default 0.5); that has been true for --scale-down-unneeded-time (default 10m); every pod can be relocated; and --scale-down-delay-after-add (default 10m) has elapsed. This is requests-based, not usage-based: a node at 4% actual CPU whose pods request 60% of allocatable will never scale down. Removal is vetoed outright by a blocking PodDisruptionBudget, kube-system pods without a PDB, pods with no controller owner, local storage (hostPath, or emptyDir without medium: Memory), or cluster-autoscaler.kubernetes.io/safe-to-evict: "false".
Karpenter: a control loop over individual nodes
Karpenter also watches unschedulable pods, then solves the inverse problem: given these pods' requests, affinities, tolerations and topology constraints, *what instance would satisfy them?* It batches pending pods, bin-packs them, filters the region's instance catalogue against the NodePool's requirements, and launches through the EC2 Fleet API (ec2:CreateFleet on AWS) using price-capacity-optimized. No Auto Scaling group is involved.
Two CRDs hold the config. A `NodePool` (karpenter.sh/v1) sets constraints: spec.template.spec.requirements (operators In, NotIn, Exists, Gt, Lt, plus a minValues floor on diversity), taints, startupTaints, expireAfter and spec.limits. A `NodeClass` — EC2NodeClass on AWS (karpenter.k8s.aws/v1), AKSNodeClass on Azure — holds infrastructure: subnetSelectorTerms, securityGroupSelectorTerms, amiSelectorTerms, IAM role, block device mappings, user data.
Scale-down is not a threshold check but consolidation, which can replace as well as remove. The disruption controller identifies candidates, checks disruption budgets, simulates whether the pods fit elsewhere, taints the node, launches the replacement before terminating the original, then drains through the Eviction API respecting PDBs. Three policies on spec.disruption.consolidationPolicy:
- `WhenEmpty` — remove only nodes holding nothing but DaemonSets and ephemeral pods.
- `Balanced` — added in v1.14. Removes empty and clearly underutilised nodes, but declines actions where the churn is not worth the saving.
- `WhenEmptyOrUnderutilized` — the default. Any node that can be removed *or replaced with something cheaper*.
consolidateAfter (default 0s) is a stability timer, and the detail people miss is that it resets every time a pod is added to or removed from the node. On a churny cluster, a large value can mean a node is never quiet long enough to become a candidate.
Where the difference actually bites
1. Scale-up latency, and why bypassing the ASG matters
Both see the same unschedulable pod at the same time; the gap opens after. CA's decision is *an API call changing an integer*, after which it waits on the Auto Scaling group's own reconciliation loop to notice the new desired capacity, pick an instance from the launch template and launch it — wall-clock time CA cannot see into, polling until a node registers or --max-node-provision-time expires. Karpenter's decision *is* the launch: a fleet call with a concrete instance spec, no second loop to wait on.
Rather than quote a benchmark, note the tradeoff AWS documents: raising CA's scan interval from 10s to 60s cuts API calls roughly sixfold at the cost of about 38% slower scale-ups. Karpenter v1.14 also added `CapacityBuffer` (alpha) for pre-provisioned headroom, replacing the pause-pod hack still standard under CA.
2. Instance flexibility, and what diversification buys on Spot
Under CA, supporting *n* instance shapes means *n* node groups, because each must be internally homogeneous. Every group is simulated on every scan, so CA's cost scales with group count — AWS explicitly calls per-team node groups an anti-pattern. Real estates settle on a few groups covering a few families, exactly the wrong posture for Spot.
Spot capacity is per instance type, per availability zone, per pool, and interruption risk is a function of how many pools you can fall back to. Three types across two AZs is six pools. Karpenter, unconstrained, considers the region's whole catalogue for a NodePool and hands EC2 that entire list, letting price-capacity-optimized draw from the deepest pools; minValues lets you *enforce* a diversity floor so a well-meaning NotIn filter cannot quietly collapse your options to two. The rule follows directly: narrowing instance types to save money on Spot usually costs money, by raising interruption frequency and forcing on-demand fallback.
3. Bin-packing and consolidation vs removal-only scale-down
Cluster Autoscaler can only remove a node. It cannot say "this workload shrank, so a smaller instance would do": its scale-down is a filter, not an optimiser. Karpenter does single-node replacement — swap a node for a cheaper one that still fits its pods — as well as multi-node consolidation onto fewer nodes.
One rule to know before it surprises you: spot-to-spot consolidation requires at least 15 cheaper instance type options than the current node, otherwise Karpenter refuses and logs why. Swapping a Spot node for a marginally cheaper one from a shallow pool trades a small saving for a large jump in interruption rate, so over-constrain a NodePool and consolidation silently does nothing on Spot. Consolidation also packs on requests, not limits: if pods burst above their memory requests, tighter packing turns accidental headroom into OOM kills. Set requests == limits for memory first.
4. Spot interruption handling
Cluster Autoscaler does not handle interruption. It reacts to the *result* — a node vanishes, pods go pending, CA scales up — so closing that gap means running AWS Node Termination Handler separately.
Karpenter handles it natively. Point --interruption-queue at an SQS queue fed by EventBridge and it consumes Spot interruption warnings, scheduled change health events, instance stop and terminate events, and instance status check failures, then taints, drains and terminates ahead of the two-minute window. Do not run Node Termination Handler alongside it — they will fight over the same nodes.
5. Drift, expiry and the upgrade story
Under CA, changing an AMI means updating a launch template then rolling nodes yourself. CA has no opinion about node age or configuration correctness; a node running the wrong AMI for eight months is invisible to it.
Karpenter treats configuration mismatch as a first-class condition. Drift marks a NodeClaim when its owning NodePool or NodeClass no longer matches it, and drifted nodes are replaced automatically. The triggering fields are specific: spec.template.spec.requirements on the NodePool, and subnetSelectorTerms, securityGroupSelectorTerms and amiSelectorTerms on the EC2NodeClass. Behavioural fields deliberately do not — changing spec.weight, spec.limits or anything under spec.disruption will not churn your fleet.
`expireAfter` (default 720h) caps node lifetime and has sharp edges. Expiration is *forceful*: once the clock runs out, draining starts regardless. Disruption budgets throttle voluntary disruption — consolidation, drift, emptiness — but do not hold back expiry. The real ceiling is expireAfter plus terminationGracePeriod, after which pods terminate whether their PDBs allow it or not. That is deliberate — no node can be pinned forever by a bad PDB — but unread it looks like Karpenter ignoring yours.
That makes drift the cleanest node upgrade mechanism available: change the AMI alias, let budgets pace the rollout. Pin AMIs explicitly (alias: al2023@v20240807) rather than @latest, or an upstream release becomes an unplanned cluster-wide rollout — which is why NodePool and NodeClass belong in version control like any other infrastructure managed with Terraform.
6. Operational complexity and the blast radius of a bad NodePool
Cluster Autoscaler has narrow authority: it moves an integer between bounds you set, and the worst realistic outcome is scaling a group to its maximum. Node group maximums are a genuine safety rail.
Karpenter has broad authority — instance types, sizes, AMIs, capacity types, disk config. A NodePool with no spec.limits and permissive requirements is authorised to launch very large, very expensive instances the moment a pod requests them, so set limits on cpu, memory and nodes for every NodePool with a billing alarm behind it. Two more edges: when several NodePools match a pod, Karpenter selects among them non-deterministically unless taints make them mutually exclusive or spec.weight orders them; and never run the Karpenter controller on nodes Karpenter manages, or you have a circular dependency that will not survive a full-cluster restart. CA has its own ceiling — AWS documents degradation beyond roughly 1,000 nodes.
7. Multi-AZ and topology constraints
Under CA, availability zone is a property of the node group. Multi-AZ groups drift out of balance and the ASG's own rebalancing terminates nodes at inconvenient moments, so standard EKS guidance is one node group per AZ plus --balance-similar-node-groups=true. That triples your group count before you express any other dimension of variation, and interacts badly with EBS, since a volume in one AZ cannot attach in another.
Karpenter treats zone as another scheduling requirement, reading topologySpreadConstraints, pod affinity and anti-affinity, and provisioning into whichever zone satisfies them — including honouring the zone of an existing EBS volume for a StatefulSet pod. Supported spread keys include topology.kubernetes.io/zone, kubernetes.io/hostname and karpenter.sh/capacity-type. One caveat: preferred spread (ScheduleAnyway) can produce *more* nodes than expected, since Karpenter will launch one to better satisfy a preference.
The comparison
| Karpenter | Cluster Autoscaler | Fixed managed node groups | |
|---|---|---|---|
| Scale-up latency | Fleet API call straight from pod requirements | Sets ASG desired capacity, waits on the ASG's own loop | None; capacity is running or it is not |
| Instance flexibility | Region catalogue per NodePool; minValues sets a diversity floor | One shape per group; mixed policies must be same-shape | Fixed at creation |
| Spot handling | Native SQS interruption queue; price-capacity-optimized; spot-to-spot gated at 15 cheaper options | Reacts after the fact; needs Node Termination Handler | Nothing reacts to interruption |
| Bin-packing | Removal *and* replacement, including downsizing | Removal only, at <50% of requests for ~10 minutes | None; sized once |
| Upgrade / drift | Drift replaces mismatched nodes; expireAfter caps age | External: launch template plus instance refresh | Node group update, on your schedule |
| Operational complexity | Broad authority; needs limits and budgets | Low; group maximums are a hard rail | Lowest |
| Ecosystem maturity | v1 stable since v1.0, now v1.14; AWS and Azure production-grade | Very mature, 30+ providers; version must match cluster | Fully mature |
| Best when | Heterogeneous or spiky workloads, heavy Spot, cost is live | Predictable workloads, strict node group semantics | Small or steady clusters, or the base layer under either |
When Cluster Autoscaler is still the right answer
Node group semantics as a contract. Some platforms treat the group as a real boundary: a billing unit, a compliance scope, a thing a runbook names. Karpenter dissolves that boundary by design, so if your organisation reasons in node groups, adopting it is an organisational change before a technical one.
Regulated environments with fixed instance families. If your control set enumerates approved instance types, Karpenter's main advantage is prohibited. You can constrain a NodePool to that list, but then you run a more complex controller to do a simpler job, having lost the Spot diversity that justified it.
Platforms where the provider is immature. AWS and Azure are solid — AKS Node Auto Provisioning is Karpenter with the Azure provider underneath, using NodePool and AKSNodeClass. Beyond those, providers for GCP, IBM Cloud, Oracle and Cluster API vary genuinely in maturity, while CA supports 30-plus and has for years.
No capacity to own another controller. Karpenter is infrastructure you now operate: upgrades, CRD migrations, NodePool review, an alert when limits are hit.
And fixed managed node groups sometimes beat both: if the cluster is small, load is flat and you are not on Spot, an autoscaler is a moving part earning nothing. Most clusters that "need autoscaling" need accurate resource requests first — autoscaling on top of wrong requests just buys the wrong nodes faster. That diagnostic is where our Kubernetes consulting work usually begins, and it ties into the wider AWS cost optimization picture.
Migration notes
You do not cut over. You run both, and make it impossible for them to fight.
They must not manage the same capacity. CA touches only ASGs tagged for discovery (k8s.io/cluster-autoscaler/enabled and k8s.io/cluster-autoscaler/<cluster-name>: owned) or listed with --nodes; Karpenter manages only nodes it created. The failure mode is CA scaling down a node whose pods Karpenter just placed, which only happens if CA can see nodes it did not create.
Steer workloads with taints, not hope. Taint the new NodePool, add the toleration to one non-critical Deployment, move it alone. Keep a node group for Karpenter itself and the cluster add-ons; if CoreDNS runs on Karpenter nodes, set the lameduck duration and readiness probe or you will see DNS blips on every consolidation.
What breaks, roughly in order of frequency:
- Pods with no resource requests. Karpenter sizes nodes from requests, so a pod requesting nothing gets a node sized for nothing, then evicts under load. The most common migration failure by a distance.
- Memory limits above requests. Consolidation packs on requests; bursty pods that survived on accidental headroom now OOM.
- Missing or too-permissive PDBs. CA's slow scale-down was hiding this. Consolidation is not slow.
- Assumptions about instance type. Anything reading
node.kubernetes.io/instance-type, hardcoded ENI limits, DaemonSets sized for one family, licences pinned to a core count. - Local storage.
emptyDirandhostPathpods CA refused to evict are ordinary pods to Karpenter.
First week, watch: node count and instance-type distribution (a sudden shift to one family means over-constrained requirements), consolidation events per hour, eviction and OOM rates, Spot interruption frequency, and whether you are hitting NodePool limits. Start on WhenEmpty, then move to Balanced or WhenEmptyOrUnderutilized. Set spec.disruption.budgets deliberately — the default nodes: 10% is reasonable on a large cluster and aggressive on a small one.
FAQ
Can I run Karpenter and Cluster Autoscaler at the same time?
Yes, and during a migration you should. They must not manage overlapping capacity: CA acts only on ASGs it discovers by tag or that you list explicitly, Karpenter only on nodes it created. Keep those sets disjoint and there is no contention. Problems appear when a node group is left in CA's discovery scope while Karpenter is also scheduling onto it.
Does Karpenter actually save money?
Usually, by a specific mechanism rather than magic: consolidation that *replaces* nodes with cheaper ones rather than only deleting them, tighter bin-packing, and Spot diversification across more pools than a node group model can express. Anyone quoting a percentage before seeing your workloads is guessing — it depends almost entirely on how accurate your resource requests already are. Padded requests mean large gains; an already well-packed cluster may save very little.
Is Karpenter AWS-only?
No, and this is the most commonly repeated outdated claim about it. Karpenter's core is a provider-agnostic project under kubernetes-sigs, with cloud-specific providers implementing the NodeClass layer. Azure's ships as AKS Node Auto Provisioning: Karpenter with NodePool and AKSNodeClass resources. Providers also exist for GCP, IBM Cloud, Oracle Cloud and Cluster API at varying maturity. Treat AWS and Azure as production-grade and evaluate the rest individually.
What breaks when consolidation is aggressive?
Three things, reliably. OOM kills, because consolidation packs on requests while pods burst against limits — set requests == limits for memory. Rescheduling storms, where consolidation evicts pods, the replacements leave another node underutilised, and it consolidates again; consolidateAfter damps this, but the timer resets on every pod add or remove, so a large value can stop consolidation entirely. Availability dips on workloads with missing PDBs, which CA's slower scale-down was hiding. Constrain the blast radius with spec.disruption.budgets, which can be scoped to specific reasons (Drifted, Underutilized, Empty) and to a schedule and duration.
How do I protect stateful workloads from consolidation?
Layered, strongest first. A PodDisruptionBudget is the correct primary control and the one Karpenter's eviction path respects. The `karpenter.sh/do-not-disrupt: "true"` annotation on a pod excludes its node from consolidation entirely, and accepts a duration such as "30m" to cover a long-running job; it works on a Node too. For a whole class of workloads, give them a dedicated NodePool with consolidationPolicy: WhenEmpty, or set budgets to nodes: "0". Two limits: none of this holds back forceful expireAfter expiration, and terminationGracePeriod eventually overrides a blocking PDB.
Do I still need node groups?
At least one. The Karpenter controller cannot run on nodes Karpenter manages without a circular dependency, so it needs a small managed node group or a Fargate profile. Most teams keep that group hosting cluster-critical add-ons — CoreDNS, metrics server, ingress controllers — where predictable placement beats optimal packing. Beyond that node groups are optional, though many estates keep one or two for workloads with hard placement requirements. Getting that split right is most of the design work in a Kubernetes platform review.
The short version: if your workloads are heterogeneous or spiky, you use Spot seriously, or you are trying to cut compute cost, Karpenter's model fits the problem and Cluster Autoscaler's does not. If load is predictable or you cannot take on another controller, Cluster Autoscaler is not a compromise. Either way, check your resource requests first — both autoscalers are only as good as the numbers they read.

