,

Deploy Karpenter on EKS: Node Auto-Scaling Guide (2026)

Learn how to deploy Karpenter on Amazon EKS with the latest v1 API and optimize Kubernetes node auto-scaling for 2026. This guide covers installation, IAM setup, Spot interruption handling, disruption budgets, and production best practices—plus explains why Karpenter alone doesn’t solve pod-level resource optimization and how to close that gap.

Kunal Das Avatar
Deploy Karpenter on EKS Node Auto-Scaling Guide Featured Image

Karpenter is an open-source Kubernetes node autoscaler that provisions EC2 instances directly via the AWS API – no Auto Scaling Groups required. When a pod goes pending, Karpenter reads its requirements, selects the optimal instance type, and calls RunInstances. Nodes are ready in 45–60 seconds (under typical conditions with AL2023 on a warm API path; custom AMIs or constrained AZs may extend to 90s+). Cluster Autoscaler’s ASG-based model takes 3–4 minutes for the same operation. This guide shows you how to deploy Karpenter on EKS using Karpenter v1.13.0, the current stable release, with a complete production-ready deployment from start to finish.

Key takeaways

  • Karpenter v1.13.0 uses the karpenter.sh/v1 API – the old v1alpha5 Provisioner is removed
  • Node provisioning: 45–60 s (Karpenter) vs. 3–4 min (Cluster Autoscaler)
  • Install via Helm from oci://public.ecr.aws/karpenter/karpenter
  • Use EKS Pod Identity (EKS 1.24+) or IRSA for controller IAM credentials
  • Karpenter cannot fix overprovisioned pod requests – that requires a separate rightsizing layer

What Karpenter does differently

Traditional Kubernetes node autoscalers work through node groups. You define an Auto Scaling Group, Cluster Autoscaler monitors unschedulable pods every 10 seconds, and requests a scale-up through the ASG API. The ASG launches an instance, the instance boots, the kubelet registers – 3–4 minutes minimum.

Karpenter removes the node group entirely. It watches the Kubernetes scheduler directly, and the moment a pod is unschedulable, Karpenter evaluates the pod’s resource requests and scheduling constraints. It picks the best-fit instance type across the full EC2 catalog – including Spot and Graviton – and calls RunInstances directly. No ASG round-trip.

The three provisioning layers

Understanding Karpenter’s architecture helps you tune it correctly. Think of it as three distinct layers:

  • Layer 1 – Event detection: Karpenter watches for Unschedulable pod events. No polling interval. Near-instant reaction.
  • Layer 2 – Instance selection: The scheduler evaluates every compatible instance type against node requirements (arch, capacity type, size constraints) defined in your NodePool. It picks the cheapest fit.
  • Layer 3 – Lifecycle management: Karpenter manages the full node lifecycle – provisioning, consolidation, expiry, and disruption budgets, through the NodeClaim abstraction.

The v1 API replaced Provisioner and AWSNodeTemplate with NodePool and EC2NodeClass. Upgrading from v0.x requires a CRD migration before you install v1.13.0.

Why teams are switching from Cluster Autoscaler in 2026

Speed is the headline difference, but it is not the only one. Here is a direct comparison:

DimensionCluster AutoscalerKarpenter v1.13.0
Provisioning speed3–4 minutes (ASG round-trip)45–60 seconds* (direct EC2 API)
Instance selectionPredefined node groupsFull EC2 catalog, per-pod selection
Bin-packingScale-down based on requested resourcesActive consolidation (WhenEmptyOrUnderutilized)
Scale-down triggerRequested resources (not actual usage)Actual utilization + scheduling simulation
Spot supportRequires separate node groups per typeNative; multi-type fallback built-in
Scale ceilingBottleneck around 500+ nodes (single replica)Horizontal controller scaling supported
AWS-native?No (generic)Yes (AWS provider is first-class)

* Provisioning times measured under typical EKS conditions with AL2023 on a warm API path. Custom AMIs with large user-data scripts, or instance types with capacity constraints in specific AZs, may extend provisioning to 90 seconds or more.

Bin-packing matters most at scale. Cluster Autoscaler scales down based on requested resources. Karpenter simulates pod placement and consolidates aggressively. On 50+ node clusters with heterogeneous workloads, that difference translates directly to wasted spend.

When not to switch: GKE (no official karpenter-provider-gcp as of mid-2026), OpenStack, IBM Cloud, or Alibaba Cloud. Also wait if your images do not support ARM64 – the NodePool below enables Graviton, which requires multi-arch builds.

Prerequisites before you deploy

  • EKS cluster running Kubernetes 1.25 or later (1.36 used in this guide)
  • AWS CLI v2, kubectl, and Helm 3 installed locally
  • aws-pod-identity-agent addon v1.3+ enabled on EKS (for Pod Identity; skip if using IRSA)
  • Sufficient IAM permissions to deploy CloudFormation stacks and create IAM roles
  • Subnets and security groups tagged with karpenter.sh/discovery: <cluster-name>

Important: Do not run Cluster Autoscaler and Karpenter simultaneously on the same node groups. Both controllers will attempt to provision nodes for unschedulable pods, causing duplicate provisioning and conflicting scale-down decisions. Disable Cluster Autoscaler (scale its Deployment to 0) before activating Karpenter’s NodePool, or use the taint-based partition approach to separate their scopes.

Tag subnets and security groups before deploying. The EC2NodeClass uses tag-based selectors to find them. Missing tags cause Karpenter to launch nothing and log a selector mismatch error.

# Tag subnets (repeat for each subnet ID)
aws ec2 create-tags \
  --resources subnet-XXXXXXXX \
  --tags Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}

# Tag security groups
aws ec2 create-tags \
  --resources sg-XXXXXXXX \
  --tags Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}

Step-by-step deployment

Five steps: IAM setup, Helm install, apply the EC2NodeClass, apply the NodePool, then smoke test. Each step must complete before the next.

Step 1: Set environment variables

export KARPENTER_NAMESPACE="kube-system"
export KARPENTER_VERSION="1.13.0"
export K8S_VERSION="1.36"
export AWS_PARTITION="aws"
export CLUSTER_NAME="${USER}-karpenter-demo"
export AWS_DEFAULT_REGION="us-east-1"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"

Step 2: Deploy IAM resources via CloudFormation

The template creates the Karpenter controller IAM role, node instance profile, SQS interruption queue, and EventBridge rules. Run it once per cluster.

aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-url "https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/website/content/en/docs/getting-started/getting-started-with-karpenter/cloudformation.yaml" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"

Key permissions granted: RunInstances, TerminateInstances, CreateFleet, RequestSpotInstances, sqs:*, and iam:PassRole. Review the template before deploying in regulated environments.

Step 2b: Bind the controller to its IAM role (Pod Identity)

The CloudFormation stack creates the IAM role but does not bind it to the Karpenter controller pod. Without this step, the controller deploys successfully but cannot call any EC2 APIs – provisioning attempts fail silently.

Pod Identity (preferred on EKS 1.24+):

# Create Pod Identity association after CloudFormation completes
aws eks create-pod-identity-association \
  --cluster-name "${CLUSTER_NAME}" \
  --role-arn "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole-${CLUSTER_NAME}" \
  --namespace "${KARPENTER_NAMESPACE}" \
  --service-account "karpenter"

# Verify the association
aws eks list-pod-identity-associations --cluster-name "${CLUSTER_NAME}"

IRSA alternative (EKS < 1.24, or if the Pod Identity addon is not available): Add the following flag to the Helm install command in Step 3:

--set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole-${CLUSTER_NAME}"

Use one or the other, not both. Pod Identity is the current recommendation for EKS 1.24+; IRSA remains fully supported for older clusters.

Step 2c: Register the KarpenterNodeRole with EKS

The CloudFormation template creates the node IAM role but does not register it with EKS. This step is required before Karpenter can provision nodes that successfully join the cluster. Without it, nodes boot but the kubelet registration fails and they never reach Ready.

EKS 1.21+ (access entries – preferred):

aws eks create-access-entry \
  --cluster-name "${CLUSTER_NAME}" \
  --principal-arn "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}" \
  --type EC2_LINUX

Older clusters using aws-auth ConfigMap:

kubectl patch configmap aws-auth -n kube-system --type merge \
  -p "{\"data\":{\"mapRoles\":\"- groups:\\n  - system:bootstrappers\\n  - system:nodes\\n  rolearn: arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}\\n  username: system:node:{{EC2PrivateDNSName}}\\n\"}}"

Access entries are the current AWS recommendation. The aws-auth ConfigMap approach still works but requires manual YAML management and is error-prone at scale.

Step 3: Install Karpenter with Helm

helm registry login public.ecr.aws --username AWS \
  --password $(aws ecr-public get-login-password --region us-east-1)

helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version "${KARPENTER_VERSION}" \
  --namespace "${KARPENTER_NAMESPACE}" --create-namespace \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${CLUSTER_NAME}" \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait

For production: Run at least 2 controller replicas with pod anti-affinity to avoid a single point of failure. Add --set replicas=2 to the Helm install command. Also ensure the controller Deployment runs on system nodes that Karpenter does not manage – use a nodeSelector targeting your system node group. A controller running on a node it manages creates a bootstrap dependency that can stall recovery if the node is disrupted.

The --set settings.interruptionQueue flag tells Karpenter which SQS queue to poll for Spot interruption notices. Use the same name as your cluster. --wait blocks until the controller pod is ready.

Step 4: Apply the EC2NodeClass

The EC2NodeClass defines the AWS-specific node configuration: AMI family, IAM role, subnets, and security groups.

kubectl apply -f - <<EOF
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  role: "KarpenterNodeRole-${CLUSTER_NAME}"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"
EOF

Step 5: Apply the NodePool

The NodePool defines scheduling requirements, disruption policy, and resource limits. This example enables Spot and on-demand, across c, m, and r families, on both x86 and Graviton.

Note: This NodePool mixes Spot and on-demand instances and enables both amd64 and arm64. It is a good starting configuration, but not production-ready as-is. Production clusters typically use separate NodePools: one for system/critical workloads (on-demand only, specific instance families), and one or more for batch/flexible workloads (Spot-first). See the Karpenter NodePool docs for multi-NodePool patterns.

kubectl apply -f - <<EOF
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64", "arm64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - 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: ["2"]
        - key: karpenter.k8s.aws/instance-size
          operator: NotIn
          values: ["nano", "micro", "small", "medium", "large"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h
      terminationGracePeriod: 24h
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 5m
    budgets:
      - nodes: "10%"
      - schedule: "0 9 * * mon-fri"  # UTC — adjust for your timezone
        duration: 8h
        nodes: "0"
  limits:
    cpu: 1000
    memory: 1000Gi
EOF

The budgets block caps disruptions at 10% of managed nodes and freezes consolidation during business hours (09:00–17:00 UTC, weekdays). Adjust the cron for your timezone.

Step 6: Run the smoke test

# Verify CRDs are registered
kubectl get nodepools
kubectl get ec2nodeclasses

# Deploy a test workload requiring 2 CPU / 4 Gi memory
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: karpenter-smoke-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: karpenter-smoke-test
  template:
    metadata:
      labels:
        app: karpenter-smoke-test
    spec:
      containers:
      - name: test
        image: public.ecr.aws/amazonlinux/amazonlinux:2
        command: ["sleep", "infinity"]
        resources:
          requests:
            cpu: "2"
            memory: "4Gi"
EOF

# Watch NodeClaim creation — should appear within seconds
kubectl get nodeclaims -A -w

# Watch the node join the cluster
kubectl get nodes -w

# Inspect provisioning decisions in the controller logs
kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter -f

A NodeClaim appears within seconds and the node reaches Ready within 60 seconds. If it does not, check controller logs for selector mismatch errors or missing IAM permissions.

Production tuning

Disruption budgets

The defaults in the NodePool above are a starting point. Tune these four settings before you take production traffic:

  • nodes: "10%" – no more than 10% of managed nodes disrupted at once. Raise or lower based on your deployment’s blast radius tolerance.
  • consolidateAfter: 5m – Karpenter waits 5 minutes of underutilization before triggering consolidation. 1 minute is too aggressive for production; workloads with burst patterns will see unnecessary churn.
  • terminationGracePeriod: 24h – long-running jobs get up to 24 hours to finish before Karpenter forces eviction on node expiry. Set this to match your longest expected job duration.
  • Schedule freeze – the schedule: "0 9 * * mon-fri", nodes: "0" budget blocks all consolidation during business hours. This prevents Karpenter from disrupting pods during peak traffic windows.

Spot interruption handling

The Helm install already set settings.interruptionQueue. Verify the SQS queue and EventBridge rules exist:

aws sqs get-queue-url --queue-name "${CLUSTER_NAME}"
aws events list-rules --name-prefix "Karpenter-${CLUSTER_NAME}"

When AWS sends a Spot interruption notice, EventBridge routes it to the SQS queue, Karpenter reads it, and cordons + drains the node before the 2-minute reclaim window closes. Without the queue, pods terminate without graceful shutdown.

Prometheus metrics to watch

Add these to your Karpenter dashboard:

  • karpenter_nodes_total – nodes under management by state
  • karpenter_nodeclaims_total – NodeClaims by lifecycle phase
  • karpenter_scheduler_scheduling_duration_seconds – time from pod pending to NodeClaim creation (your latency baseline – alert on p95 > 90s)
  • karpenter_interruption_received_messages_total – Spot interruption notices processed
  • karpenter_nodeclaims_disrupted_total – nodes disrupted by consolidation or expiry
  • karpenter_voluntary_disruption_queue_depth – pending consolidation backlog

If karpenter_voluntary_disruption_queue_depth stays high, check for PodDisruptionBudgets blocking eviction – see the gotchas below.

PDB and StatefulSet gotchas

Two issues silently stall consolidation in production:

  • PDB with maxUnavailable: 0 – Karpenter cannot evict any pod from the node. Consolidation stalls without an error. Audit with kubectl get pdb -A before enabling WhenEmptyOrUnderutilized.
  • StatefulSet with EBS RWO volumes – if Karpenter tries to consolidate a node and the replacement lands in a different AZ, the drain fails because the EBS volume cannot attach cross-AZ. Pin StatefulSet workloads to a topology spread or use a dedicated NodePool with AZ constraints.

Pausing/rolling back Karpenter safely

Incidents happen. You need a fast, safe way to stop Karpenter without disrupting running workloads. Run these steps in order, each one is independently reversible.

# Step 1: Stop new provisioning — set resource limits to zero
kubectl patch nodepool default --type=merge \
  -p '{"spec":{"limits":{"cpu":"0","memory":"0"}}}'

# Step 2: Stop consolidation — freeze disruption budget
kubectl patch nodepool default --type=merge \
  -p '{"spec":{"disruption":{"budgets":[{"nodes":"0"}]}}}'

# Step 3: Scale down the controller if needed
kubectl scale deployment karpenter -n kube-system --replicas=0

# Step 4: Restore Cluster Autoscaler if needed
kubectl scale deployment cluster-autoscaler -n kube-system --replicas=1

Steps 1 and 2 leave existing nodes running. Karpenter stops making decisions but terminates nothing. Step 3 fully disables the controller. Existing nodes continue running; they become unmanaged until you scale the controller back up.

Where Karpenter’s coverage ends

Karpenter excels at node provisioning. It does not solve the full Kubernetes cost stack.

The core limitation: Karpenter trusts pod resource requests. If a pod requests 4 CPU but uses 0.4, Karpenter provisions a node sized for 4 CPU. It has no mechanism to measure actual consumption or adjust requests. That is a deliberate design boundary, not a bug.

The Cast AI 2026 Kubernetes Optimization Report found the average cluster runs at 8% CPU utilization and 20% memory utilization. CPU overprovisioning reached 69% (up from 40% YoY); memory overprovisioning sits at 79%. These gaps live entirely in pod request declarations. Karpenter sees inflated requests and provisions accordingly.

Karpenter also has no visibility into Spot interruption rates by instance family or cross-account cost allocation. At scale, those gaps compound.

How Cast AI complements Karpenter

Cast AI operates as an optimization layer above Karpenter’s provisioning. The integration is called KENT (Cast AI’s Karpenter Enterprise integration layer), and it adds three things Karpenter does not have:

  • Pod rightsizing – Cast AI measures actual CPU and memory consumption per container and adjusts resource requests automatically. This reduces the inflated inputs Karpenter provisions against.
  • Spot prediction – models per-instance interruption probability and routes workloads to lower-interruption pools.
  • Cost visibility – real-time cost attribution per namespace, workload, and team.

Cast AI connects in read-only mode first. No changes happen until you enable them. Teams running the full Cast AI stack on top of Karpenter report a 43% average compute cost reduction (Cast AI 2026 report; customer range: 40–70%). Connect in read-only mode to see your cluster’s overprovisioning in about five minutes.

Kubernetes cost benchmark data

The following numbers come from the Cast AI 2026 Kubernetes Optimization Report, which analyzed anonymized data from production clusters. These are fleet-wide averages, not outliers.

  • 8% average CPU utilization (down from 10% in 2025)
  • 20% average memory utilization
  • 69% CPU overprovisioning – pods request 69% more CPU than they consume (up from 40% YoY)
  • 79% memory overprovisioning
  • 43% compute cost reduction with full optimization (Cast AI stack on top of Karpenter)

Karpenter brings provisioning speed and bin-packing gains. But at 69% CPU overprovisioning, the nodes it provisions are still oversized relative to actual workload needs. Karpenter is optimal for the inputs it receives. Fix the inputs to close the remaining gap.

FAQ

Does Karpenter replace Cluster Autoscaler entirely?

On EKS, run one or the other – not both. Both watch for unschedulable pods and will conflict. Disable Cluster Autoscaler before enabling Karpenter.

What Kubernetes versions does Karpenter v1.13.0 support?

Kubernetes 1.25 and later. The 2026 getting-started guide uses 1.36. Check the karpenter.sh compatibility matrix before upgrading.

Can I run Karpenter on GKE or AKS?

AKS has an official provider. GKE does not have an official karpenter-provider-gcp as of mid-2026. Stay on Cluster Autoscaler on GKE until an official provider ships.

What happens to Karpenter-managed nodes if I scale the controller to zero?

Existing nodes keep running but become unmanaged – no new provisioning, no consolidation, no expiry. Scale the controller back up to resume management.

How do I prevent consolidation from disrupting production workloads?

Use PodDisruptionBudgets on critical deployments, the schedule-based freeze budget (nodes: "0" during business hours), and consolidateAfter: 5m. Audit for maxUnavailable: 0 PDBs – they silently stall consolidation without an error.

Is consolidationPolicy: WhenEmptyOrUnderutilized safe for production?

Yes, with disruption budgets in place. Without them, it can trigger multiple simultaneous drains. The NodePool in this guide caps disruptions at 10% of managed nodes and freezes consolidation during business hours. Apply those budgets first.

Cast AIBlogDeploy Karpenter on EKS: Node Auto-Scaling Guide (2026)