Kubernetes Autoscaling Explained (2026): HPA, VPA, KEDA and Karpenter

Oct 20, 2026 · 13 min · Ajay Kumar

Most Kubernetes autoscaling problems I get asked about are not autoscaler bugs. They are two control loops disagreeing, or one loop doing exactly what it was told with numbers nobody checked. Kubernetes autoscaling in 2026 is four separate systems that happen to share a cluster: the Horizontal Pod Autoscaler changes replica counts, the Vertical Pod Autoscaler changes requests, KEDA feeds events into the HPA and handles zero, and a node autoscaler (Cluster Autoscaler, Karpenter, or a cloud's managed version of one) adds and removes machines. This post explains what each one does today, what changed this year (HPA scale to zero went beta in 1.37, in-place resize is stable, Karpenter is on 1.15), how they interact, and the mistakes that show up on the bill.

The four layers of Kubernetes autoscaling

It helps to draw the stack before choosing tools. Pod-level autoscalers react to load. Node-level autoscalers react to pods. Nothing reacts to the node autoscaler except your invoice.

  • HPA (built in, autoscaling/v2) watches a metric and changes replicas on a Deployment, StatefulSet or anything with a scale subresource.
  • VPA (a separate project in kubernetes/autoscaler) watches historical usage and changes CPU and memory requests.
  • KEDA (kedacore/keda) is an operator that turns external event sources (queues, streams, databases, cron) into metrics the HPA can read, and owns the 0 to 1 transition.
  • Node autoscalers watch for pods the scheduler cannot place and provision capacity, then remove nodes that are empty or underused.

The chain is causal. A queue grows, KEDA reports it, the HPA adds replicas, the scheduler fails to place some, the node autoscaler launches a machine, the pods start. Every arrow in that chain has its own polling interval and its own delay, which is why "we autoscale" and "we absorb a spike in 30 seconds" are different claims.

Default autoscaler timers, in seconds (upstream defaults) scale-up loop scale-down wait Cluster Autoscaler scan-interval 10 HPA sync period 15 KEDA pollingInterval 30 HPA scaleDown stabilization 300 KEDA cooldownPeriod (to zero) 300 CA scale-down-unneeded-time 600 CA scale-down-delay-after-add 600 CA max-node-provision-time 900 0300600900 Up is checked every 10 to 30 s. Down waits 5 to 10 min on purpose. Plan cost around the down side.
Upstream defaults from the Kubernetes HPA docs, the KEDA 2.21 ScaledObject spec and the Cluster Autoscaler FAQ. Managed services may override them.

How the Horizontal Pod Autoscaler works in 2026

The HPA is a loop in kube-controller-manager that runs every 15 seconds by default (--horizontal-pod-autoscaler-sync-period), according to the HPA documentation. The core formula has not changed in years: desiredReplicas = ceil(currentReplicas * currentMetricValue / desiredMetricValue). Utilization targets are a percentage of the container's requests, which is the single most misunderstood fact in Kubernetes autoscaling. If a container has no CPU request, the HPA cannot compute utilization for it and takes no action. If the request is wildly wrong, the HPA scales on a wrong number with total confidence. (I wrote more about this in Kubernetes requests and limits explained.)

The behavior field is where you tune it. The defaults, quoted from the docs, are: scale-up has no stabilization window and may add the larger of 100 percent or 4 pods every 15 seconds; scale-down uses a 300-second stabilization window and may remove up to 100 percent of pods per 15 seconds. The stabilization window means the HPA picks the highest recommendation it computed during the last five minutes before it scales down, which is what stops flapping.

Two 2026 changes matter:

  1. Configurable tolerance is stable. The HPA ignores changes within 10 percent of the target by default. The HPAConfigurableTolerance gate was alpha in 1.33, beta in 1.35 and is stable in 1.37, so you can now set tolerance per direction under behavior.scaleUp and behavior.scaleDown. For large deployments, 10 percent of 200 pods is 20 pods of slack, which is often too much on the way up.
  2. Scale to zero is beta and on by default. The HPAScaleToZero gate sat in alpha from 1.16 to 1.36. In 1.37 it became beta, enabled by default. It only works with object or external metrics, because CPU and memory cannot be measured on pods that do not exist, and the HPA records a ScaledToZero condition so it can tell its own zero apart from a human setting replicas to 0.

Here is a realistic HPA for a web service plus a KEDA ScaledObject for a worker, both written against current APIs:

# Kubernetes 1.37 (tolerance is GA there; on 1.35-1.36 it needs the beta gate enabled)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 3            # keep one per zone; zero is for workers, not front doors
  maxReplicas: 60
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 65   # percent of the CPU *request*
  behavior:
    scaleUp:
      tolerance: 0.03            # react to a 3% overshoot instead of the default 10%
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 20                # shed at most 20% of pods per minute
        periodSeconds: 60
---
# KEDA 2.21: queue worker that scales to zero when idle
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: invoice-worker
spec:
  scaleTargetRef:
    name: invoice-worker
  minReplicaCount: 0
  maxReplicaCount: 50
  pollingInterval: 30        # default; how often KEDA checks the queue at zero
  cooldownPeriod: 300        # default; idle time before going 1 -> 0
  triggers:
  - type: rabbitmq
    metadata:
      queueName: invoices
      mode: QueueLength
      value: "20"            # target messages per replica
    authenticationRef:
      name: rabbitmq-auth

VPA in 2026: in-place resize changes the trade-off

The Vertical Pod Autoscaler used to have one awkward property: to change a pod's requests it had to evict the pod. That made it fine for batch jobs and nerve-racking for anything stateful. Two things changed. In-place pod resize, first shipped as alpha in 1.27, is stable since Kubernetes 1.35 and can no longer be disabled. And the VPA learned to use it.

The current VPA quickstart lists six updateMode values: Off, Initial, Recreate (the default), InPlaceOrRecreate, InPlace (never evicts, added in 1.7), and Auto, which is now deprecated and means Recreate. The latest release, VPA 1.8.0 from 23 September 2026, adds reactive CPU startup-boost unboosting and fixes a bug where an OOM event near an aggregation boundary was discarded instead of triggering a memory bump.

The rule that has not changed is in the VPA's own known limitations: do not run VPA and HPA on the same resource metric. If the HPA scales on CPU utilization and the VPA raises the CPU request, utilization drops, the HPA removes pods, utilization rises, and the two loops chase each other. VPA on memory with HPA on CPU, or HPA on an external metric, is fine. My default is VPA in Off mode everywhere as a recommendation engine, and automatic modes only where I have watched the recommendations for a couple of weeks.

KEDA vs HPA: when you need event-driven autoscaling

KEDA does not replace the HPA. It creates and manages one. A ScaledObject becomes an HPA plus an external metrics adapter, and KEDA itself only handles activation (0 to 1) and deactivation (1 to 0), per the ScaledObject spec. Everything between 1 and N is the HPA.

You need KEDA when the signal is outside the pod: queue depth, consumer lag, a Prometheus query, a cron window. The 2.21 docs list about 80 scaler pages, and KEDA 2.21.0, released 23 September 2026, adds ClickHouse, Cloud Spanner and Cosmos DB change feed scalers. It also contains three breaking changes, one of which is a fix for a critical advisory (CVE-2026-77524) about service account token audiences in TriggerAuthentication. If you use Vault Kubernetes auth or boundServiceAccountToken, read the migration guide before upgrading. Treating the autoscaler as a privileged component with an attack surface is exactly the kind of platform discipline that does not get less important because the workloads are now AI agents.

Now that the HPA can scale to zero on its own, is KEDA still needed? For many cases, yes. Native scale to zero still needs someone to serve an external metric, which in practice is often KEDA or a Prometheus adapter. KEDA also brings ScaledJobs for run-to-completion work and the scaler catalogue. If you already have an external metrics adapter and a single queue, the native path is now viable. HTTP scale to zero remains awkward: the KEDA HTTP add-on is at v0.16.0 and still labelled beta.

Cluster Autoscaler vs Karpenter in 2026

The Cluster Autoscaler works on node groups you define (ASGs, managed instance groups, VM scale sets). Every 10 seconds it looks for unschedulable pods, simulates adding a node from each group, and grows the best one; it removes nodes whose requested CPU and memory sit below 50 percent of allocatable for 10 minutes, per the FAQ. It supports many clouds, it is boring, and boring is a feature. The newest release line is 1.36.1 (July 2026); match its minor version to your cluster.

Karpenter skips node groups. It reads pending pods' requirements, picks instance types directly from the provider's catalogue, launches them, and continuously consolidates: deleting empty nodes and replacing underused ones with cheaper ones. Core is at v1.15.0 (10 October 2026), which adds default topology-spread awareness and a pod event for insufficient capacity errors. Since v1.14 there is a third consolidationPolicy, Balanced, which only consolidates when the savings outweigh the pod disruption, sitting between WhenEmpty and the default WhenEmptyOrUnderutilized.

Autoscaler Scales Current version (Oct 2026) Where it runs Source
HPA Replicas Kubernetes 1.37 (zero is beta) Every cluster k8s docs
VPA Requests 1.8.0 Add-on release
KEDA Replicas from events, 0 to N 2.21.0 Add-on (operator) release
Cluster Autoscaler Node groups 1.36.1 Most clouds release
Karpenter Individual nodes 1.15.0 core, AWS 1.15.0, Azure 1.14.5 AWS, Azure, community providers README
GKE node auto-provisioning Node pools Workload-level via ComputeClass from 1.33.3-gke.1136000 GKE GKE docs
AKS node auto-provisioning Nodes (managed Karpenter) Default in AKS Automatic AKS Microsoft Learn
EKS Auto Mode Nodes (managed Karpenter) 21-day max node lifetime EKS AWS docs

Karpenter's provider list is longer than people expect. The project README lists AWS and Azure as first-party, plus Cluster API, IBM Cloud, Oracle, Alibaba, a community GCP provider, Hetzner, Proxmox and others. On GKE, though, the sensible choice is Google's own node auto-provisioning.

Node auto-provisioning on GKE, AKS and EKS

All three big clouds now sell the Karpenter idea as a managed feature, so for many teams the choice is no longer "Cluster Autoscaler or Karpenter" but "which knobs does my provider expose".

  • GKE has had node auto-provisioning for years. From 1.33.3-gke.1136000 you can enable it per workload through a custom ComputeClass with nodePoolAutoCreation, instead of cluster-wide with global CPU and memory limits.
  • AKS node auto-provisioning is built on Karpenter and the AKS Karpenter provider, configured with NodePool and AKSNodeClass, and comes preconfigured in AKS Automatic. It does not support Windows node pools, IPv6 clusters or service principals.
  • EKS Auto Mode relies on Karpenter on locked-down Bottlerocket nodes and replaces nodes after at most 21 days. You add custom NodePools; you do not edit the defaults.

The trade is the usual one: less to operate, less to tune, and someone else's release cadence.

Scale to zero: what it buys and what it costs

Scale to zero is where the autoscaling story meets FinOps. Idle replicas cost money only if they keep nodes alive, so pod-level zero pays off only when the node autoscaler then removes the node, which by default takes another 10 minutes on the Cluster Autoscaler and depends on consolidateAfter in Karpenter. The cost is cold start: KEDA must notice (up to pollingInterval), the scheduler must place the pod, possibly a node must boot, and the image must pull. I have written about what scale to zero cost me to build at the microVM layer of PandaStack, my own Firecracker cloud; the same arithmetic applies in Kubernetes, just with slower parts. Use zero for queue consumers, batch and GPU jobs. Keep a floor for anything a human waits on.

Common Kubernetes autoscaling mistakes

  • No requests, or fiction for requests. HPA utilization and every node autoscaler's bin-packing run on requests. Fix requests first.
  • HPA and VPA on the same metric. Covered above. Pick one per resource.
  • minReplicas: 1 behind a PodDisruptionBudget with minAvailable: 1. Nodes can never drain, so consolidation and scale-down stall.
  • Scale-down too aggressive. A 60-second window on a spiky service means scaling down, then paying the cold start again.
  • Ignoring zones. Topology spread plus a node autoscaler limited to one zone gives you pods stuck in Pending. My Pending pods guide walks through that case.
  • No max. maxReplicas and NodePool limits are the only things standing between a metrics bug and a very large invoice.

FAQ

What is the difference between HPA and VPA?

The HPA changes how many pods run; the VPA changes how much CPU and memory each pod requests. HPA suits stateless services that scale out, VPA suits workloads that cannot. Do not run both on the same resource metric.

Is Karpenter better than Cluster Autoscaler?

Karpenter usually packs nodes more tightly and reacts faster because it chooses instance types per batch of pods rather than growing predefined groups. Cluster Autoscaler supports more providers and is simpler to reason about. On EKS and AKS the managed Karpenter (Auto Mode, NAP) is now the default path; on GKE, use node auto-provisioning.

Can Kubernetes scale to zero without KEDA?

Yes, from Kubernetes 1.37 the HPA can set minReplicas: 0 by default, but only with object or external metrics. You still need something to serve those metrics, and KEDA remains the easiest way to do that for queues and streams.

Do I need KEDA if I only scale on CPU?

No. Plain HPA autoscaling/v2 handles CPU and memory. KEDA earns its place when the signal lives outside the pod or you need to reach zero.

This quarter I would do three things on any cluster I was responsible for: run VPA in Off mode to audit requests, set explicit behavior and maxReplicas on every HPA, and move node autoscaling to whatever managed Karpenter-style option the provider offers, keeping a hard limits block on every NodePool. For the companion troubleshooting steps see the Pending pods guide, and for the money side, Kubernetes cost optimization with OpenCost and Karpenter.

Related: Kubernetes as the Agent Control Plane in 2026, What Scale to Zero Cost Me to Build and FinOps for AI in 2026.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related