KubernetesAdvanced

Karpenter: Faster, Simpler Kubernetes Autoscaling on AWS

What Karpenter actually does differently from the Cluster Autoscaler, how it provisions nodes in seconds instead of minutes, and how to configure it safely on EKS.

DevFieldGuideJuly 31, 20269 min read
Share:

The Kubernetes Cluster Autoscaler works by scaling predefined node groups up or down. Karpenter throws that model out — it provisions exactly the right node, from scratch, in response to a specific unschedulable pod. That difference sounds small. In practice it changes both how fast your cluster scales and how much you pay for it.

The problem with node-group-based autoscaling

The Cluster Autoscaler operates on Auto Scaling Groups (or equivalent node pools). Each group is defined ahead of time with a fixed instance type, and the autoscaler simply adds or removes instances from whichever group matches an unschedulable pod's requirements. This means:

  • You maintain multiple node groups per instance type/size combination you might need (a group for m5.xlarge, another for m5.2xlarge, another for GPU nodes, and so on).
  • A pod that needs slightly more CPU than any existing group provides either doesn't get scheduled efficiently, or forces you to pre-provision a bigger group "just in case."
  • Scaling latency includes the Auto Scaling Group's own scaling activity, which typically takes minutes, not seconds.
Prerequisites
  • A working EKS cluster with kubectl access and cluster-admin permissions.
  • Familiarity with Kubernetes scheduling concepts (requests, taints/tolerations, node affinity).
  • An IAM OIDC provider already associated with the cluster (required for Karpenter's IAM role setup).

How Karpenter actually decides what to launch

Karpenter watches for pods stuck in Pending because nothing in the cluster satisfies their scheduling constraints. Instead of picking from a fixed menu of node groups, it looks at exactly what the pending pod(s) need — CPU, memory, architecture, GPU, zone — and asks the cloud provider's API directly for a matching instance type, right then.

Pod goes PendingNo existing node satisfies its requests/constraints
Karpenter evaluatesChecks NodePool constraints against pod requirements
Instance selectionPicks a matching, available EC2 instance type in real time
Node launchesBypasses ASGs — calls EC2 Fleet/RunInstances directly
Pod schedulesTypically under a minute end-to-end

This is why Karpenter is usually so much faster: it isn't waiting on an Auto Scaling Group's own scaling lifecycle. It calls the EC2 Fleet API directly to launch exactly the instance type the pending workload needs.

Setting up a NodePool and EC2NodeClass

Karpenter's configuration is two Kubernetes custom resources working together. The EC2NodeClass describes how nodes should be configured at the AWS level (AMI, subnets, security groups, IAM role). The NodePool describes what Karpenter is allowed to provision and under what constraints.

yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  role: "KarpenterNodeRole-my-cluster"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "my-cluster"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "my-cluster"
yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 30s

The subnet and security group karpenter.sh/discovery tags aren't cosmetic — Karpenter uses them at launch time to find where new nodes are allowed to live. Forgetting to tag your subnets is one of the most common reasons a fresh Karpenter install silently fails to provision anything.

Consolidation: the part that actually saves money

Provisioning the right node on demand is half the story. The other half is Karpenter continuously re-evaluating whether the current set of nodes is still the cheapest valid layout, and consolidating when it isn't.

consolidationPolicy: WhenEmptyOrUnderutilized tells Karpenter to actively look for opportunities to replace multiple underutilized nodes with fewer, better-packed ones, or terminate nodes that have gone empty — not just scale down when load drops, but actively repack. This is a meaningful behavioral difference from the Cluster Autoscaler, which only removes nodes that are already empty; it never proactively repacks a partially-utilized cluster onto fewer nodes.

Before consolidation: 4 nodes, each 30% utilized After consolidation: 2 nodes, each ~60% utilized

Spot instances without the operational pain

Karpenter has first-class spot support built into the NodePool requirements shown above (capacity-type: spot). When AWS reclaims a spot instance, Karpenter receives the interruption notice, cordons and drains the node, and provisions a replacement — automatically, without a separate spot-handling controller like node-termination-handler (though many teams still run it for faster, more graceful draining on the 2-minute interruption warning).

Mixing spot and on-demand in the same NodePool (as in the example above) lets Karpenter default to whichever is available and cheapest for a given launch, while still falling back to on-demand under demand for capacity.

Setting real limits so Karpenter doesn't over-provision

limits.cpu in the NodePool spec is a hard ceiling on how much compute that pool can ever provision — without it, a runaway deployment or a misconfigured HPA can trigger unbounded node provisioning and an unbounded bill. Every production NodePool should have an explicit limit, sized to what you'd actually approve if someone asked for it manually.

Best practices
  1. Set limits.cpu (and optionally limits.memory) on every NodePool — an unbounded pool turns a bug in your workloads into an unbounded AWS bill.
  2. Use multiple NodePools to express real policy differences — a spot-only pool for fault-tolerant batch work, an on-demand-only pool for anything that can't tolerate interruption, gated by taints and tolerations.
  3. Tag subnets and security groups with the exact karpenter.sh/discovery value your EC2NodeClass selectors expect — this is the single most common first-install failure.
  4. Start with consolidateAfter: 30s1m rather than disabling consolidation, then widen it if you see nodes churning too aggressively for genuinely bursty workloads.

Monitoring what Karpenter is actually doing

Karpenter emits Kubernetes events on the pods and nodes it acts on, which is the fastest way to understand a scaling decision without digging through controller logs:

bash
kubectl get events --field-selector reason=Nominated -A
kubectl describe node <node-name> | grep -A5 "karpenter.sh"

It also exposes Prometheus metrics on the controller's metrics endpoint — karpenter_nodes_created_total, karpenter_pods_state, and karpenter_nodeclaims_terminated_total are the ones worth wiring into a dashboard first. karpenter_nodeclaims_terminated_total broken down by reason is particularly useful for catching a consolidation loop early — nodes being created and terminated on a tight cycle usually points at a consolidateAfter value that's too aggressive for a genuinely bursty workload, not a bug in Karpenter itself.

For a slower-than-expected scale-up, the diagnostic order that actually finds the cause fastest:

  1. Check whether the pod is Pending for a scheduling reason unrelated to capacity at all — an unsatisfiable node selector or a missing toleration produces a pod that will never schedule no matter how many nodes Karpenter launches.
  2. Check NodePool requirements against what the pod actually needs — a NodePool restricted to arm64 will never satisfy an amd64-only pod.
  3. Check EC2 service quotas for the account/region — Karpenter can request an instance type successfully rejected by EC2 itself if a quota is exhausted, and that failure surfaces as an event on the NodeClaim, not always somewhere obvious.

Interruption handling in more detail

When AWS reclaims a spot instance, it sends a two-minute interruption notice via the EC2 instance metadata service before actually terminating it. Karpenter's built-in interruption handling (enabled via an SQS queue configured on the controller) watches for this notice and immediately begins cordoning and draining the node — evicting pods so they reschedule elsewhere before the hard termination happens, rather than after.

yaml
apiVersion: karpenter.sh/v1
kind: NodePool
spec:
  disruption:
    expireAfter: 720h  # force node replacement after 30 days regardless

expireAfter is a separate mechanism from consolidation — it forces periodic node replacement even on a fully-utilized, otherwise-stable node, which matters for keeping AMIs and kernel patches current across a fleet that might otherwise never naturally churn.

Common mistakes

Common mistakes
  • Running Karpenter and the Cluster Autoscaler against the same node groups simultaneously. Both controllers try to make scaling decisions independently, and they can fight each other — migrate node groups to Karpenter-managed NodePools deliberately, not incrementally on the same group.
  • Leaving limits.cpu unset on a NodePool. A bug that spawns thousands of pods will happily be matched by thousands of new nodes unless there's an explicit ceiling.
  • Forgetting that consolidation actively terminates and replaces underutilized nodes — for workloads sensitive to any restart (some stateful sets, long-running batch jobs), that needs an explicit do-not-disrupt annotation on the pod, not just a hope Karpenter leaves it alone.
  • Assuming Karpenter handles pod disruption budgets automatically without a real PDB defined. Karpenter respects PodDisruptionBudgets during consolidation, but only if you've actually set one — without it, consolidation can terminate more replicas at once than your service can tolerate.

Karpenter vs. the Cluster Autoscaler, at a glance

AspectCluster AutoscalerKarpenter
Provisioning unitScales predefined Auto Scaling GroupsLaunches individual EC2 instances directly, no ASG required
Instance selectionLimited to whichever groups you pre-createdPicks from the full range of matching instance types at launch time
Typical scale-up latencySeveral minutes (ASG scaling lifecycle)Often under a minute
Bin-packing / consolidationRemoves empty nodes onlyActively repacks underutilized nodes onto fewer, cheaper ones
Spot handlingRequires separate tooling per node groupNative interruption handling built into the controller

Is Karpenter worth adopting?

For any EKS cluster running variable or bursty workloads, the honest answer is close to yes by default now — AWS itself steers new EKS users toward Karpenter over the Cluster Autoscaler. The migration effort is real (rewriting node groups as NodePools, retagging subnets and security groups, testing consolidation behavior against your actual workloads before trusting it in production) but it's a one-time cost against an ongoing win in both scale-up latency and idle-capacity waste.

Frequently Asked Questions

DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in Kubernetes

View all