If you're running Kubernetes on AWS, compute is almost certainly your largest line item. And the autoscaler you choose — Cluster Autoscaler or Karpenter — determines whether you're paying for what you use or paying for what you might use. After migrating a dozen EKS clusters from Cluster Autoscaler to Karpenter over the past year, we've seen teams cut compute spend by 25–40% without sacrificing availability. But Karpenter isn't always the right answer. Here's how to decide.

How Cluster Autoscaler Works (and Where It Falls Short)

Cluster Autoscaler (CA) has been the default Kubernetes node autoscaler since 2016. It watches for pods that can't be scheduled due to insufficient resources, then adds nodes from pre-configured Auto Scaling Groups (ASGs). When nodes sit idle, it removes them.

The fundamental problem is that CA operates at the node group level, not the pod level. You define ASGs with specific instance types — say, m5.xlarge in one group and c5.2xlarge in another — and CA picks the group that has capacity. It doesn't reason about the actual resource request of the pending pod.

This leads to a pattern we see in nearly every CA-managed cluster: over-provisioning by default. A pod requesting 500m CPU and 1Gi memory triggers the launch of a 4-vCPU, 16GiB node because that's what the ASG is configured to provide. The remaining 3.5 vCPUs sit empty until something else gets scheduled — if anything does.

Teams try to work around this by creating more node groups (small, medium, large, GPU, etc.), but this introduces its own complexity. Each ASG needs separate scaling policies, capacity reservations, and monitoring. We've seen clusters with 15+ node groups where engineers spend more time managing ASG configurations than deploying applications.

Karpenter's Fundamentally Different Approach

Karpenter, open-sourced by AWS in 2021 and now a CNCF sandbox project, takes a different approach entirely. Instead of mapping pods to pre-defined node groups, it looks at the pending pod's actual resource requests and constraints, then provisions the cheapest instance that fits.

There are no ASGs. Karpenter talks directly to the EC2 Fleet API. When a pod needs 2 vCPUs and 4GiB RAM, Karpenter can launch a c6g.large (2 vCPU, 4GiB) instead of the m5.xlarge that CA would have chosen. When three pods each need 1 vCPU, Karpenter might bin-pack them onto a single m6i.xlarge instead of launching three separate nodes.

The key abstractions are NodePools (formerly Provisioners) and NodeClasses (formerly AWSNodeTemplates). A NodePool defines constraints: what instance families are acceptable, what capacity type (on-demand vs spot), what availability zones, and resource limits. A NodeClass defines the AWS-specific details: AMI family, subnets, security groups, and block device mappings.

Here's a typical Karpenter NodePool that gives it broad flexibility to optimize:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: general-workloads
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64", "arm64"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["4"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: "400"
    memory: 800Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 60s

Notice what's not there: no specific instance types. Karpenter picks from the entire c/m/r family across generations 5, 6, and 7, in both x86 and ARM architectures, using spot when available. That's potentially hundreds of instance type and AZ combinations — far more than any set of ASGs would cover.

Consolidation: Where the Real Savings Come From

Launching right-sized nodes is only half the story. The other half is consolidation — Karpenter's ability to continuously re-evaluate running nodes and replace them with cheaper alternatives.

With CA, once a node is running, it stays until it's empty. Karpenter actively looks for opportunities to consolidate. If you have three m5.xlarge nodes each running a single pod that needs 1 vCPU, Karpenter can cordon those nodes, launch a single m5.xlarge that fits all three pods, and terminate the originals.

The consolidateAfter: 60s setting in the NodePool above means Karpenter will act on consolidation opportunities within 60 seconds. You can tune this based on your tolerance for pod disruption — we typically recommend 30–120 seconds for most workloads.

Key insight: Consolidation is the single biggest cost lever. In our migrations, roughly 60% of the savings come from consolidation, not from initial right-sizing. Clusters that seem well-tuned under CA often have 30–40% wasted capacity hiding in partially-filled nodes that CA never reclaims.

Spot Instance Handling: A Major Differentiator

Both CA and Karpenter support Spot instances, but the experience is dramatically different.

With CA, you create separate ASGs for Spot and configure the --expander=priority flag to prefer Spot over on-demand. But each ASG can only specify a handful of instance types for its Spot pool, and when Spot capacity gets interrupted, CA has to wait for the ASG to find replacement capacity in its limited instance list.

Karpenter's approach is more resilient. Because it considers the full universe of instance types matching your constraints, it has a much deeper Spot pool to draw from. When a c6i.xlarge Spot instance gets interrupted, Karpenter might replace it with a c6a.xlarge, c5a.xlarge, or even an m6g.xlarge (ARM) — whatever is cheapest and available right now.

We've measured Spot interruption recovery times of 15–30 seconds with Karpenter versus 2–5 minutes with CA. For latency-sensitive workloads, that's the difference between a blip and an incident.

When Cluster Autoscaler Is Still the Right Choice

Karpenter isn't a universal upgrade. There are legitimate reasons to stick with CA:

Migration Strategy: Running Both in Parallel

The safest migration path is to run Karpenter alongside CA, gradually shifting workloads. Here's the approach we use:

  1. Install Karpenter alongside CA. Deploy Karpenter with a NodePool that has low resource limits (e.g., 20 CPUs). Taint its nodes with a custom taint so existing workloads stay on CA-managed nodes.
  2. Migrate non-critical workloads first. Add tolerations and nodeAffinity to dev/staging workloads to prefer Karpenter nodes. Monitor scheduling latency, pod startup times, and Spot interruption handling for 1–2 weeks.
  3. Raise Karpenter limits, lower CA capacity. Gradually increase Karpenter's CPU/memory limits while reducing the maxSize on CA's ASGs. This naturally shifts new pods to Karpenter as CA's headroom shrinks.
  4. Drain remaining CA node groups. Once you're confident in Karpenter's behavior, cordon CA-managed nodes and let pods reschedule onto Karpenter nodes during normal deployments.
  5. Remove CA. Delete the CA deployment and ASGs. Keep the ASG configurations in your IaC code (commented out) for 30 days in case you need to roll back.

The entire migration typically takes 3–6 weeks for a production cluster. Don't rush it — the parallel running period is where you discover edge cases like pods with anti-affinity rules that don't play well with Karpenter's bin-packing, or DaemonSets that consume more resources than you realized.

Measuring the Impact

Before you migrate, establish baselines so you can quantify the improvement. The metrics that matter:

Here's a quick way to check your current instance type distribution:

kubectl get nodes -o json | \
  jq -r '.items[] | .metadata.labels["node.kubernetes.io/instance-type"]' | \
  sort | uniq -c | sort -rn

A healthy Karpenter cluster should show 8–15 different instance types — evidence that it's actively optimizing across the broadest possible set.

The Bottom Line

For most EKS clusters running 20+ nodes with variable workloads, Karpenter delivers meaningful cost savings with better Spot resilience and simpler configuration. The migration isn't trivial, but it's well-understood and low-risk when done incrementally. If you're still running Cluster Autoscaler and haven't evaluated Karpenter in the last six months, the compute savings alone make it worth a serious look.

The teams that get the most out of Karpenter are the ones who pair it with proper resource requests on their pods. Karpenter can only right-size nodes if pods honestly declare what they need. If your pods are requesting 4Gi of memory but actually using 500Mi, no autoscaler can save you from yourself — that's a resource governance problem that needs to be solved first.