devops

Karpenter vs Cluster Autoscaler: Which Cuts EKS Node Cost More?

Karpenter vs Cluster Autoscaler compared on node cost, speed, and spot handling. Real configs, bin-packing math, and when to pick each on EKS in 2026.

July 25, 2026·7 min read·
#kubernetes#autoscaling#cost-optimization#aws#finops

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:

ApproachNodesMonthly compute (us-east-1, rough)
CA, single m6i.xlarge group, on-demand~13 × m6i.xlarge~$1,990
Karpenter, mixed c/m/r, on-demand + consolidationright-sized set~$1,450
Karpenter, spot-first for stateless tiermostly 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 Pending if no allowed instance can host them — for example a pod requesting a GPU when your pool only allows c/m/r. Karpenter's wide requirements make this less common, but a too-narrow nodeSelector or an un-tolerated taint will still strand pods. Debugging that path is exactly the Pod stuck in Pending / FailedScheduling workflow.

  • Consolidation churn. Aggressive consolidationAfter can evict and reschedule pods often. Protect critical workloads with PodDisruptionBudgets and set do-not-disrupt annotations 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 situationPick
On AWS/EKS, mixed pod shapes, want lowest costKarpenter
Heavy spot usage across many instance typesKarpenter
Need node provisioning under ~45s for spiky trafficKarpenter
Multi-cloud or on GKE/AKS where CA is the native pathCluster Autoscaler
Uniform workloads, few node shapes, want minimal moving partsCluster Autoscaler
Strict, audited node images with one blessed instance typeCluster 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.

#kubernetes#autoscaling#cost-optimization#aws#finops
D
DevToCashAuthor

Senior DevOps/SRE Engineer · 10+ years · Professional Trader (IDX, Crypto, US Equities)

I write about real infrastructure patterns and trading strategies I use in production and in live markets. No courses, no affiliate hype — just documentation of what actually works.

More about me →