Back to Blog
AWSEKSKarpenterreduce_costs

Karpenter versus Cluster Autoscaler: why per-pod provisioning cuts EKS node cost

How Karpenter replaces node-group round-up with per-pod instance selection, bin-packing, and continuous consolidation to reliably shave 30-60% off EKS compute.

JohannaMarch 15, 20266 min read

Most EKS clusters do not overspend because someone chose the wrong instance type. They overspend because Cluster Autoscaler is structurally forced to buy capacity in the shape you guessed at months ago, then keep buying more of that same shape whether or not it fits what is actually pending. Karpenter attacks the problem from the other end: it looks at the pods that cannot schedule right now and provisions the cheapest EC2 capacity that satisfies them. That single inversion is where the 30-60% comes from, and it is worth understanding the mechanism rather than treating the number as a slogan.

The node-group round-up tax

Cluster Autoscaler (CA) does not talk to EC2 directly. It manages Auto Scaling Groups, one per node group, and each node group is a fixed template: an instance type (or a set of types with identical resources) chosen when the group was created. When pods go unschedulable, CA simulates which node group would fit them and increments that group's desired count. EC2 then launches a whole node of the predefined type.

The waste is baked into that flow. If a node group is built on m5.4xlarge (16 vCPU, 64 GiB) and the pending work needs 3 vCPU, CA still adds a full m5.4xlarge. You now pay for 16 vCPU to run 3. Bin-packing across node groups is coarse because CA reasons at the group level, and the group's granularity is one large instance. Over a quarter, fragmentation like this accumulates into nodes that sit 20-40% utilized with no automated path to shrink them.

Karpenter skips node groups entirely. It watches for pods in Pending, aggregates their combined CPU, memory, architecture, and topology constraints, and calls the EC2 Fleet API (CreateFleet) to launch instances chosen from a broad, open set of types. If the pending set needs 3 vCPU and 6 GiB, Karpenter can launch a c7g.xlarge or a small burstable, not a fixed jumbo node. The instance is selected to fit the work, not the other way around.

Bin-packing and continuous consolidation

Two behaviors do the heavy lifting.

First, bin-packing on provisioning. When several pods are pending at once, Karpenter treats them as a batch and packs them onto as few nodes as possible before launching anything, then picks instance types that fit that packing tightly. You get fewer, better-filled nodes from the first scheduling decision.

Second, continuous consolidation, which CA has no real equivalent for. Karpenter periodically re-evaluates the whole fleet and asks two questions: can any node be removed, and can any node be replaced by something cheaper. Set it with consolidationPolicy: WhenEmptyOrUnderutilized and a consolidateAfter window. It acts in three ways:

  • Emptiness — a node whose pods have all drained is cordoned and terminated.
  • Single-node consolidation — an underused node's pods are rescheduled elsewhere, then the node is removed.
  • Replacement — a node is swapped for a cheaper instance type that still fits its pods (for example, collapsing two half-full nodes onto one right-sized node, or moving on-demand work onto a cheaper family).

Every consolidation is a real drain-reschedule-terminate cycle. Karpenter cordons the node, evicts pods through the Kubernetes eviction API so they reschedule onto existing or newly launched capacity, and only then terminates the instance. This is why a Karpenter cluster's utilization drifts up over time instead of decaying — fragmentation gets reclaimed automatically.

NodePools and constraints

You express intent through a NodePool (the constraints and disruption policy) paired with an EC2NodeClass (AMI, subnets, security groups, IAM). The NodePool is where you keep Karpenter's freedom bounded. A typical set of requirements:

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"]

Widening the allowed set is what makes Spot viable and consolidation effective — the more instance types Karpenter may choose from, the better it packs and the deeper it discounts. Narrow NodePools (one family, one size) recreate the node-group problem you were trying to escape.

Spot-first, with interruption handling that actually drains

List spot before on-demand in the capacity-type requirement and Karpenter prefers Spot, launching it with the price-capacity-optimized allocation strategy so it favors pools that are both cheap and deep. Spot on its own is up to ~90% off On-Demand, so a Spot-heavy NodePool is often the single biggest line-item mover.

The part people skip is interruption hygiene. Point Karpenter at an SQS queue subscribed to EventBridge, and it receives Spot interruption warnings, rebalance recommendations, instance state-change, and scheduled-maintenance events. On a two-minute Spot interruption warning it proactively cordons and drains the doomed node so pods reschedule onto replacement capacity before the instance disappears. Without that queue wired up, an interruption is an abrupt kill and your pods eat the full restart penalty.

The eviction hygiene Karpenter demands

Because consolidation and Spot both move pods constantly, a Karpenter cluster punishes applications that are careless about shutdown. Non-negotiables:

  • PodDisruptionBudgets on anything with availability requirements. Karpenter respects PDBs during consolidation and interruption draining; a minAvailable that keeps quorum prevents it from taking down too many replicas of a service at once.
  • Honor SIGTERM. When a pod is evicted it gets SIGTERM, then SIGKILL after the grace period. Applications that ignore SIGTERM drop in-flight requests on every move.
  • A realistic terminationGracePeriodSeconds so connection draining and checkpoint writes finish. On the node side, the NodePool's terminationGracePeriod caps how long Karpenter waits for a node to drain before forcing termination.
  • karpenter.sh/do-not-disrupt on pods that genuinely must not be moved mid-flight — a long batch job with no checkpointing, for instance.

Get this wrong and consolidation savings show up as intermittent 5xx spikes, which is a worse trade than the money you saved.

EC2 quota gotchas

Karpenter's appetite for instance diversity runs straight into EC2 service quotas, and these fail quietly — pods stay Pending with an insufficient-capacity or limit error rather than anything obvious. Watch:

  • Running On-Demand Standard instances is a single vCPU-denominated quota covering the A, C, D, H, I, M, R, T, Z families combined. A burst can hit it even though no single family looks large.
  • All Standard Spot Instance Requests is a separate vCPU quota. Spot-heavy clusters exhaust it independently of On-Demand.
  • Fleet and CreateFleet request-rate limits can throttle rapid scale-ups.

Raise the On-Demand and Spot vCPU quotas before you need them, and keep NodePool limits set so a runaway workload cannot scale into a surprise five-figure hour.

A concrete mixed cluster

Take a cluster with two personalities: a web tier (steady traffic, latency-sensitive, needs headroom) and a batch tier (bursty, fully interruptible). Model them as two NodePools.

  • web — capacity-type on-demand (or Spot with a generous PDB), instance categories c and m, arm64 allowed so Karpenter can land Graviton when it is cheaper, consolidateAfter set conservatively so scale-down does not thrash user-facing pods.
  • batch — capacity-type spot first, a wide family set, aggressive consolidation, jobs that checkpoint and carry no PDB so Karpenter can reclaim them freely.

Under Cluster Autoscaler this cluster would run two ASGs sized for peak, both perpetually half-empty between bursts. Under Karpenter the batch NodePool scales to near zero when idle, runs on deeply discounted Spot when busy, and the web NodePool consolidates onto tightly packed, right-sized (often Graviton) nodes. Right-sizing removes the round-up tax, bin-packing removes fragmentation, Spot removes the On-Demand premium on interruptible work, and consolidation keeps all three honest as load shifts. Stack those and 30-60% off the node bill is the normal outcome, not a best case — provided you paid the eviction-hygiene and quota tax up front.