Kubernetes Autoscaling 2025 — HPA, VPA, KEDA, and Cluster Autoscaler
Advertisement
Introduction
Why This Matters
Static replica counts are a liability in production. Under-provisioning causes latency spikes and errors during traffic peaks. Over-provisioning wastes cloud spend — often 30–50% of compute costs are idle capacity that autoscaling would reclaim.
Kubernetes provides several autoscaling mechanisms that work at different levels: pod replicas (HPA), pod resource allocation (VPA), event-driven scaling (KEDA), and node count (Cluster Autoscaler). Production clusters typically use all four in combination.
In 2025, cost pressure on cloud bills has made autoscaling one of the highest-ROI investments in any Kubernetes platform. Teams that implement proper autoscaling typically reduce cloud spend by 20–40% while improving availability.
Horizontal Pod Autoscaler (HPA)
HPA scales the number of Pod replicas based on observed metrics. The most common trigger is CPU utilization, but HPA v2 supports memory and custom metrics.
Prerequisites
HPA requires the Metrics Server:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# Verify
kubectl top nodes
kubectl top pods -n productionCPU-Based HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # scale up when avg CPU > 70%kubectl apply -f hpa.yaml
kubectl get hpa -n production -w
kubectl describe hpa api-hpa -n productionCPU + Memory HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # wait 60s before scaling up again
policies:
- type: Pods
value: 4
periodSeconds: 60 # add up to 4 pods per 60 seconds
scaleDown:
stabilizationWindowSeconds: 300 # wait 5 min before scaling down
policies:
- type: Pods
value: 2
periodSeconds: 60 # remove at most 2 pods per 60 secondsThe behavior field prevents thrashing — rapid scale-up and scale-down cycles that cause instability.
Custom Metrics HPA (HTTP Requests Per Second)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-rps-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 30
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000" # target 1000 RPS per podThis requires Prometheus Adapter or a custom metrics adapter to expose http_requests_per_second from Prometheus.
Vertical Pod Autoscaler (VPA)
VPA adjusts the CPU and memory requests and limits of containers based on actual usage. It right-sizes containers, reducing waste from over-provisioning.
# Install VPA
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.shapiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Auto" # automatically applies recommendations (requires pod restart)
# Options: "Off" (recommend only), "Initial" (apply on new pods only), "Auto"
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 2
memory: 2Gi
controlledResources: ["cpu", "memory"]# Check VPA recommendations (works in any mode)
kubectl describe vpa api-vpa -n production
# Shows: Lower Bound, Target, Upper Bound recommendationsHPA + VPA caveat: Do not use HPA on CPU and VPA on CPU simultaneously — they conflict. Use VPA in Recommend mode for CPU right-sizing, and HPA for replica scaling.
KEDA — Kubernetes Event-Driven Autoscaling
KEDA scales deployments based on event sources: queue depth, Kafka lag, database query results, cron schedules, HTTP request rate, and 60+ other scalers.
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespaceScale on SQS Queue Depth
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-scaler
namespace: production
spec:
scaleTargetRef:
name: queue-worker
minReplicaCount: 0 # KEDA can scale to zero!
maxReplicaCount: 50
cooldownPeriod: 300 # seconds before scaling to zero
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123456/my-queue
queueLength: "10" # scale up when > 10 messages per pod
awsRegion: us-east-1
authenticationRef:
name: keda-aws-credentialsScale on Kafka Consumer Lag
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: kafka-consumer-scaler
namespace: production
spec:
scaleTargetRef:
name: kafka-consumer
minReplicaCount: 1
maxReplicaCount: 20
triggers:
- type: kafka
metadata:
bootstrapServers: kafka.production.svc:9092
consumerGroup: my-consumer-group
topic: events
lagThreshold: "100" # 100 messages of lag per podScale on Cron Schedule (Predictive Scaling)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: api-cron-scaler
namespace: production
spec:
scaleTargetRef:
name: api
triggers:
- type: cron
metadata:
timezone: America/New_York
start: "0 8 * * 1-5" # 8am weekdays
end: "0 20 * * 1-5" # 8pm weekdays
desiredReplicas: "10"
- type: cron
metadata:
timezone: America/New_York
start: "0 20 * * 1-5" # 8pm weekdays (off hours)
end: "0 8 * * 1-5" # 8am next day
desiredReplicas: "2"Cluster Autoscaler — Scaling Nodes
Cluster Autoscaler adds nodes when pods cannot be scheduled (insufficient resources) and removes nodes when they are underutilized for 10 minutes.
AWS EKS Setup
# cluster-autoscaler deployment (simplified)
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-autoscaler
namespace: kube-system
spec:
template:
spec:
containers:
- image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.29.0
name: cluster-autoscaler
command:
- ./cluster-autoscaler
- --cloud-provider=aws
- --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/my-cluster
- --scale-down-utilization-threshold=0.5 # remove nodes below 50% utilization
- --skip-nodes-with-local-storage=false
- --expander=least-wasteTag your Auto Scaling Groups for discovery:
# Terraform
resource "aws_autoscaling_group" "workers" {
tag {
key = "k8s.io/cluster-autoscaler/enabled"
value = "true"
propagate_at_launch = true
}
tag {
key = "k8s.io/cluster-autoscaler/my-cluster"
value = "owned"
propagate_at_launch = true
}
}Karpenter (AWS Alternative, Faster)
Karpenter provisions nodes in 60 seconds vs Cluster Autoscaler's 3–5 minutes:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand", "spot"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.large", "m5.xlarge", "m5.2xlarge"]
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 30sMonitoring Autoscaling
# HPA status
kubectl get hpa -A
kubectl describe hpa api-hpa -n production
# Pod scaling events
kubectl get events -n production --sort-by='.lastTimestamp' | grep -i scale
# Resource usage
kubectl top pods -n production --sort-by=cpu
# KEDA ScaledObject status
kubectl get scaledobject -n productionCommon Mistakes
- Setting
minReplicas: 1— one pod is still a single point of failure; use at least 2 - Not configuring scale-down stabilization — HPA can thrash (scale up, scale down, scale up) within minutes
- Using HPA on CPU without setting CPU
requests— HPA calculates utilization as a percentage of requests; without requests, HPA does not work - Scaling to zero without a scale-from-zero trigger — pods must be triggered to wake up (KEDA handles this; HPA does not)
- Not testing autoscaling behavior under load before production — use tools like k6 or Locust to simulate traffic
Best Practices
- Set HPA
minReplicasto at least 2 for high-availability; use PodDisruptionBudgets to prevent all replicas from being removed during node drain - Use stabilization windows to prevent HPA thrashing during brief load spikes
- Use KEDA for queue and event-driven workloads — it scales to zero, reducing costs for sporadic jobs
- Combine HPA (replicas) with VPA in
Recommendmode (right-size requests) — do not use both on the same metric - Use Karpenter on AWS EKS instead of Cluster Autoscaler — faster provisioning and better bin-packing
Key Takeaways
- HPA scales pod replicas based on CPU, memory, or custom metrics — requires Metrics Server and CPU/memory requests to be set
- HPA
behaviorwith stabilization windows prevents thrashing during brief traffic spikes - VPA right-sizes container resource requests and limits based on historical usage — reduces waste from over-provisioning
- KEDA enables event-driven autoscaling from SQS, Kafka, cron, HTTP, and 60+ other sources — and can scale to zero
- Cluster Autoscaler adds and removes nodes based on scheduling pressure and utilization thresholds
- Karpenter (AWS) provisions nodes 3–5x faster than Cluster Autoscaler and supports Spot interruption handling
- Autoscaling requires proper CPU requests on all containers — without requests, HPA cannot calculate utilization correctly
- Testing autoscaling with load tests (k6, Locust) before production is essential — autoscaling behavior under real load is unpredictable without testing
Advertisement