Cluster Autoscaler vs Karpenter: Node Autoscaling Without Cost Surprises
DevOps Engineering

Cluster Autoscaler vs Karpenter: Node Autoscaling Without Cost Surprises

Cluster Autoscaler vs Karpenter compared, with NodePool limits, consolidation, Spot and the guardrails that stop cost spikes.

Michael Zion

12 min read

Cluster Autoscaler adds and removes nodes in node groups you define in advance. Karpenter skips node groups and launches whichever instance type best fits the pending pods, then consolidates underused nodes. On AWS, Karpenter usually packs workloads tighter and handles Spot better. Cluster Autoscaler is the portable choice that works on every major cloud. Either way, cap total capacity explicitly.

Node autoscaling is where Kubernetes cost surprises usually come from. A sensible pod autoscaler plus an unbounded node autoscaler can turn one traffic spike into a much larger bill that never scales back down. This guide compares the two node autoscalers, shows the configuration that bounds each one, and covers the workload-level guardrails that matter with either.

Cluster Autoscaler vs Karpenter at a glance

Cluster AutoscalerKarpenter
How it adds capacityRaises the desired size of an existing node group (an Auto Scaling group, managed node group or VM scale set)Creates instances directly from pending pod requirements, choosing from many instance types
Instance choiceFixed per node group; you pre-create groups for each shapeAny type allowed by NodePool requirements (family, size, architecture, zone, Spot or On-Demand)
Scale-downRemoves nodes below a utilization threshold (default 50%) after they stay unneeded for a while (default 10 minutes)Consolidation deletes empty nodes and replaces underused ones with cheaper, smaller nodes
Spot handlingMixed-instance node groups plus a separate interruption handlerBuilt-in Spot and On-Demand selection, with interruption handling through an SQS queue
Disruption controlsPDBs and the cluster-autoscaler.kubernetes.io/safe-to-evict: "false" annotationPDBs, NodePool disruption budgets, and the karpenter.sh/do-not-disrupt: "true" annotation
Cost ceilingNode group max sizes and --max-nodes-totalspec.limits on each NodePool (CPU and memory)
CloudsAWS, GCP, Azure and many othersAWS (v1 stable since 2024); on Azure through AKS Node Auto Provisioning, which is built on Karpenter
Main riskMany node groups to maintain; slow, coarse scalingUnbounded NodePools; churn from aggressive consolidation
Two lanes compare node autoscalers: Cluster Autoscaler turns pending pods into a larger node group with a fixed node shape, capped by max size and --max-nodes-total; Karpenter creates best-fit instances directly, capped by NodePool spec.limits.
Both tools react to pending pods, but only the ceiling you set stops the growth.

Bound Karpenter with NodePool limits and disruption budgets

A Karpenter NodePool without limits can keep launching nodes as long as pods are pending. Always set a CPU and memory ceiling, choose the consolidation policy, and add a disruption budget so consolidation never drains too much of the cluster at once.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: general
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64", "arm64"]
      expireAfter: 720h          # replace nodes every 30 days
  limits:
    cpu: "200"                   # hard ceiling for this NodePool
    memory: 400Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 5m
    budgets:
      - nodes: "10%"             # never disrupt more than 10% of nodes at once
      - nodes: "0"               # no voluntary disruption during business peak
        schedule: "0 9 * * mon-fri"
        duration: 8h
Timeline of a weekday under the example NodePool budgets: up to 10% of nodes can be disrupted at once outside business hours, and 0 nodes from 09:00 to 17:00 under the schedule 0 9 * * mon-fri with duration 8h.
Consolidation keeps saving money at night but leaves the cluster alone during the business peak.

When both Spot and On-Demand are allowed, Karpenter prefers Spot and falls back to On-Demand when Spot capacity is unavailable. Allowing both architectures lets it pick cheaper Graviton instances when your images are multi-arch. Mark pods that must not be moved by consolidation (long batch jobs, singletons during a migration) with karpenter.sh/do-not-disrupt: "true".

Bound Cluster Autoscaler with node group sizes and scale-down settings

Cluster Autoscaler can only grow node groups up to their configured maximum, so the max size of each group is your cost ceiling. The flags below make scale-down and expansion choices explicit:

command:
  - ./cluster-autoscaler
  - --cloud-provider=aws
  - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/my-cluster
  - --expander=least-waste            # pick the group that wastes the least CPU and memory
  - --balance-similar-node-groups     # spread across zones evenly
  - --max-nodes-total=60              # cluster-wide ceiling
  - --scale-down-utilization-threshold=0.5
  - --scale-down-unneeded-time=10m
  - --skip-nodes-with-system-pods=true

Run one node group per zone for workloads with zonal volumes, and keep the number of node shapes small. If you need many instance types to chase Spot capacity, that is usually the point where Karpenter becomes simpler to operate.

Which one should you use?

  • Use Karpenter on EKS when workloads vary in shape, you want Spot with automatic fallback, or you are paying for half-empty nodes that Cluster Autoscaler will not consolidate.
  • Use Cluster Autoscaler when you run on several clouds, on GKE (which has its own node auto-provisioning), when a few uniform node groups already fit your workloads, or when your team prefers predictable, fixed node shapes.
  • On AKS, use the built-in cluster autoscaler or Node Auto Provisioning rather than installing either tool yourself.

Do not run both against the same nodes. When migrating from Cluster Autoscaler to Karpenter, create the NodePool, scale Cluster Autoscaler to zero replicas, then cordon and drain the old node groups gradually while PDBs protect critical workloads.

Understand Which Kubernetes Layer Is Scaling

Kubernetes autoscaling usually involves several independent control loops. They react to different signals and can amplify one another when configured carelessly.

  • Horizontal Pod Autoscaler (HPA): Changes the number of replicas for a workload, usually from CPU, memory, or a custom metric.
  • Vertical Pod Autoscaler (VPA): Recommends or changes CPU and memory requests for pods. Its operating mode needs careful review because changing requests can restart or reschedule workloads.
  • Cluster Autoscaler: Adjusts the number of nodes when pods cannot be scheduled or nodes are underused, depending on its configuration.
  • Karpenter: Provisions and removes nodes based on pending pod requirements and configured constraints.
  • Managed node pool autoscaling: Changes the size of a cloud provider’s node group according to its own minimum and maximum settings.

These layers do not share a single cost budget. An HPA can increase replicas, which creates unschedulable pods, which causes a node autoscaler to add nodes. If the HPA never scales down, or if pod requests are much larger than actual usage, the node count can remain high after traffic falls.

Chain diagram: a traffic spike makes the HPA add replicas, which creates unschedulable pods, which makes the node autoscaler add nodes; nodes stay after traffic falls when the HPA never scales down or requests are much larger than usage.
Each loop does its own job correctly, and the bill grows anyway.

Start by writing down the scaling path for each important workload:

  1. What metric increases the replica count?
  2. What is the minimum and maximum replica count?
  3. What CPU and memory requests does each replica reserve?
  4. Which node pool can run the workload?
  5. What causes a new node to be created?
  6. What prevents the workload from consuming unlimited capacity?

If you cannot answer these questions, changing a maximum replica count is not a cost-control strategy. It is a guess.

Set Resource Requests Before Setting Replica Limits

Autoscaling decisions depend on resource requests. The HPA commonly evaluates CPU utilization as a percentage of the CPU request, not as a percentage of the node's total capacity. A container with a request of 500 millicores running at 400 millicores is at 80% requested CPU, even if the node still has substantial unused capacity.

A container with a 500 millicore CPU request using 400 millicores is at 80% requested CPU, even though the node around it has substantial unused capacity.
Inaccurate requests skew every scaling decision the HPA makes.

That makes inaccurate requests expensive in both directions:

  • Requests that are too low: The scheduler packs too many pods onto a node, and the workload may experience throttling or memory pressure before the HPA reacts.
  • Requests that are too high: Each replica reserves more node capacity than it needs, so scaling out creates nodes sooner and keeps them larger than necessary.
  • Missing requests: CPU-based utilization may be unsuitable or unavailable for the workload, and scheduling becomes less predictable.

Use measured usage from representative periods, including deployments, batch activity, and peak traffic. Do not set requests from a single quiet hour. For memory, leave enough room for normal working-set variation because memory pressure can terminate a pod before a CPU-based HPA responds.

A minimal workload definition should make the resource assumptions explicit:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: example/api:1.0.0
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "1"
              memory: "512Mi"

The image name in this example is illustrative. Replace it with the image and version used by your workload. The important part is that requests and limits reflect a tested operating range rather than arbitrary defaults.

Bound the pod autoscaler too

Node autoscalers react to pending pods, so an unbounded Horizontal Pod Autoscaler drives node cost directly. Give every HPA a deliberate maxReplicas, slow scale-down (a stabilization window of about 5 minutes), and a metric that reflects real demand. A slow downstream dependency can raise CPU, add replicas, and add nodes without helping users at all. Our Kubernetes HPA guide covers the configuration in detail.

Put hard limits around namespaces and node growth

Pod limits do not automatically create a cloud cost limit. If an HPA can create ten times as many replicas, a node autoscaler may add enough capacity to schedule them. You need limits at both the workload and cluster levels.

For each node pool or provisioner, define:

  • A minimum size that supports normal operation and planned disruption.
  • A maximum size that reflects the approved capacity and cost envelope.
  • The instance types or node sizes that the workload may use.
  • Scheduling rules that keep specialized workloads away from general-purpose capacity.
  • A scale-down policy that removes genuinely unused nodes without disrupting critical workloads.

Use a ResourceQuota in each namespace to prevent one team or environment from consuming all available capacity. Pair it with a LimitRange when you need default requests and limits for containers that omit them.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: application-quota
  namespace: production
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "50"

A quota is a scheduling guardrail, not a guarantee that the cloud bill will stay below a specific amount. Node overhead, system workloads, daemon sets, storage, load balancers, and other services still consume resources. Leave capacity for those components when choosing quota values.

Review node autoscaling alongside workload placement. A pod with a restrictive node selector, taint requirement, or topology rule may force the provisioner to create a new node even when another node has spare capacity. This can be the correct availability decision, but it should be visible in the cost review.

Teams operating several clusters should document their node-pool limits and ownership clearly. For example, importing or consolidating clusters can expose inconsistent autoscaling rules that were previously hidden by separate cloud accounts or environments. A documented migration plan, such as the approach described in this Kubernetes cluster import case study, can help keep infrastructure policy explicit during that work.

Test Failure Modes Before Production Traffic Finds Them

Autoscaling should be tested as a system rather than as an isolated HPA object. A useful test starts with a known workload and observes every step: metric change, replica change, pending pods, node creation, application recovery, and scale-down.

Run these checks in a non-production environment or during an approved production exercise:

  1. Increase request load until the HPA reaches several replica levels.
  2. Confirm that new pods become ready and that readiness probes prevent traffic from reaching unready instances.
  3. Observe whether pending pods trigger node growth as expected.
  4. Stop the load and measure how long replicas and nodes take to fall.
  5. Check whether scale-down causes evictions, queue loss, connection failures, or latency spikes.
  6. Repeat the test while a deployment, node drain, or dependency slowdown is occurring.

Watch these signals during the test:

  • HPA desired replicas and current replicas.
  • Pending pods and their scheduling events.
  • Node count, node utilization, and unschedulable nodes.
  • CPU throttling and memory pressure.
  • Request latency, error rate, queue depth, and saturation.
  • Replica and node count after demand returns to normal.

Do not judge success by node utilization alone. A cluster can look efficient while users experience high latency because the application is waiting on a database or another service. Conversely, a low-utilization node may be necessary because the workload has strict placement rules or requires spare capacity for disruption.

Create a Cost Review That Matches Scaling Behavior

Cost control fails when engineers review the bill after the fact and cannot connect it to a scaling event. Record the decisions that can increase capacity and assign an owner to each one.

At minimum, review:

  • Changes to HPA minimum and maximum replicas.
  • Changes to CPU and memory requests.
  • Changes to node-pool or provisioner limits.
  • New taints, tolerations, selectors, or topology constraints.
  • Changes to custom metrics and their collection intervals.
  • Namespaces that approach their resource quotas.

Use separate limits for development, staging, and production. A staging environment that inherits production replica limits can create unnecessary spend during load tests or a stuck job. Development clusters often need a firm upper bound because an accidental deployment can otherwise remain active until someone notices.

When a workload needs more capacity, ask which constraint is actually failing:

  • If requests are too high, recalibrate them from representative usage.
  • If the HPA reacts to the wrong signal, change the metric rather than raising the maximum.
  • If pods cannot fit efficiently, review placement rules and node shapes.
  • If the workload has a legitimate sustained increase, update the capacity plan and budget deliberately.
  • If several clusters use different policies, standardize the policy before copying another configuration.
Decision map for a workload that needs more capacity: requests too high, recalibrate from representative usage; HPA reacts to the wrong signal, change the metric not the maximum; pods cannot fit efficiently, review placement rules and node shapes; legitimate sustained increase, update the capacity plan and budget; clusters use different policies, standardize the policy first.
Raising a limit is the right answer only for a legitimate sustained increase.

For teams managing AWS and Kubernetes together, infrastructure changes should be reviewed as one capacity decision rather than as separate application and cloud changes. The practices in this AWS and Kubernetes management case study are relevant to that type of review. Managed Kubernetes services such as Azure Kubernetes Service can reduce control-plane administration, but they do not remove the need to bound workload and node scaling.

When the Configuration Needs a Larger Review

Get a second review when autoscaling has become difficult to explain, when several teams share a cluster, or when a single traffic event can materially affect infrastructure spend. The review should examine manifests, metrics, scheduling events, quotas, node limits, and recent cost changes together.

Useful deliverables include:

  • A diagram showing the HPA, scheduler, node autoscaler, node pools, and external dependencies.
  • A table listing minimum and maximum replicas for each production workload.
  • A resource-request review based on observed usage.
  • A namespace quota and ownership map.
  • A test record showing scale-up and scale-down behavior.
  • An escalation rule for reaching a capacity or spending threshold.

Infrastructure teams can also compare patterns used in production Kubernetes environments, including the operational approach described in this scalable Kubernetes and cloud infrastructure case study. Treat those patterns as material for review, not as a substitute for testing your own workload.

FAQ

Is Karpenter better than Cluster Autoscaler?

On AWS it is usually more cost-efficient, because it picks right-sized instances per workload, uses Spot with fallback, and consolidates underused nodes. Cluster Autoscaler is simpler to reason about, supports every major cloud, and is fine when a few uniform node groups fit your workloads.

Does Karpenter work on Azure or GCP?

On Azure, yes: AKS Node Auto Provisioning is built on Karpenter and is managed for you. There is no supported Karpenter provider for GKE; use GKE's cluster autoscaler and node auto-provisioning there.

How do I stop Karpenter from overspending?

Set spec.limits for CPU and memory on every NodePool, restrict instance categories and sizes in requirements, enable consolidation, keep accurate pod requests, and alert on node count and spend. Without limits, any burst of pending pods can launch new nodes.

Why is my Cluster Autoscaler not scaling down?

Common causes: pods with local storage or the safe-to-evict: "false" annotation, PDBs that allow 0 disruptions, kube-system pods without PDBs, nodes above the utilization threshold, or pods that cannot fit on any other node because of affinity, taints or zonal volumes. Check the autoscaler logs and the cluster-autoscaler-status ConfigMap.

Can I run Karpenter and Cluster Autoscaler together?

Only on separate sets of nodes, and only temporarily during a migration. If both manage the same capacity they make conflicting decisions. Scale Cluster Autoscaler to zero once Karpenter NodePools are serving the workloads.

Takeaway

Pick the node autoscaler that fits your cloud and workload mix, then set autoscaling limits in layers. Give workloads accurate resource requests, bound HPA replicas, cap node-pool growth, use namespace quotas, and test the complete path from demand to cloud capacity. Then review scaling changes as capacity decisions with clear ownership. This approach keeps autoscaling useful during real demand without allowing an HPA or node provisioner to define your cloud bill by accident.