Your app works great with 2 replicas at normal traffic. But at peak hours it gets 10x more requests. And at 3 AM it gets almost none.
Running 10 replicas around the clock wastes money. Running 2 replicas at peak is too slow.
Kubernetes can scale your application automatically — up when traffic is high, down when it is quiet. This tutorial covers three autoscaling tools.
Three Types of Scaling
| Type | What scales | Tool |
|---|---|---|
| Horizontal | Number of Pods | HPA, KEDA |
| Vertical | CPU/RAM per Pod | VPA |
| Cluster | Number of nodes | Cluster Autoscaler |
Horizontal scaling adds more copies of your app. Vertical scaling makes each copy bigger. Cluster autoscaling adds more machines to the cluster when all machines are full.
Install Metrics Server
HPA needs the metrics-server to get CPU and memory usage. Install it first:
# On minikube
minikube addons enable metrics-server
# On other clusters
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# Verify
kubectl top pods
kubectl top nodes
If kubectl top pods returns values, metrics-server is working.
Horizontal Pod Autoscaler (HPA)
HPA watches CPU and memory usage and adjusts the number of Pod replicas automatically.
Setting Resource Requests (Required)
HPA needs resource requests defined on the container. Without them, HPA cannot calculate utilization.
containers:
- name: myapp
image: myapp:1.0
resources:
requests:
cpu: "200m" # HPA uses this as the baseline for CPU %
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
Creating an HPA
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # Scale up when average CPU > 60%
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75 # Scale up when average memory > 75%
kubectl apply -f hpa.yaml
kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
myapp-hpa Deployment/myapp 45%/60%, 50%/75% 2 10 2
Note: Use autoscaling/v2 for CPU and custom metrics scaling. The v1 API still works for CPU-only, but v2 is preferred for all new deployments.
Testing HPA
Generate load to trigger scaling:
# In one terminal — run a load generator
kubectl run load-test --image=busybox --restart=Never -- \
/bin/sh -c "while true; do wget -q -O- http://myapp-service; done"
# In another terminal — watch the HPA
kubectl get hpa myapp-hpa -w
You should see REPLICAS increase as CPU crosses 60%.
# Clean up
kubectl delete pod load-test
As load drops, HPA will scale back down after a cooldown period (default: 5 minutes down-scale).
HPA Behavior Tuning
Control how fast HPA scales up and down:
spec:
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # Minimum time between scale-up decisions
policies:
- type: Pods
value: 4 # Max 4 new Pods per scale-up event
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # Wait 5 min before scaling down
policies:
- type: Percent
value: 25 # Remove max 25% of Pods per step
periodSeconds: 60
Vertical Pod Autoscaler (VPA)
HPA adds more Pods. VPA adjusts the CPU and memory of existing Pods.
Use VPA to:
- Find the right resource requests for a new service
- Automatically resize long-running Pods
Installing VPA
VPA is not built into Kubernetes. Install it:
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh
Creating a VPA
# vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: myapp-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
updatePolicy:
updateMode: "Off" # Only recommend — don't change anything yet
resourcePolicy:
containerPolicies:
- containerName: myapp
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 2
memory: 2Gi
Start with updateMode: "Off" to only get recommendations:
kubectl apply -f vpa.yaml
kubectl describe vpa myapp-vpa
Look for the Recommendation section:
Recommendation:
Container Recommendations:
Container Name: myapp
Lower Bound:
Cpu: 25m
Memory: 62Mi
Target:
Cpu: 100m # VPA suggests 100m CPU
Memory: 128Mi # VPA suggests 128Mi memory
Upper Bound:
Cpu: 500m
Memory: 512Mi
Update your Deployment to use VPA’s recommended values. Use updateMode: Recreate for automatic pod restarts, or updateMode: InPlaceOrRecreate (VPA 1.4+) to attempt in-place resize before falling back to recreation. The Auto mode is now an alias for Recreate.
K8s 1.35: In-Place Pod Resize (GA)
In Kubernetes 1.35, in-place Pod resize is generally available. This is a major improvement.
Before 1.35, VPA could only resize by restarting Pods. For databases and stateful apps, restarts cause downtime.
With in-place resize, VPA (or you manually) can change a Pod’s CPU and memory without restarting the container.
VPA 1.35 introduces the InPlaceOrRecreate update mode (beta):
spec:
updatePolicy:
updateMode: "InPlaceOrRecreate" # Try in-place first, restart only if required
With this mode:
- VPA tries to resize the container in place (no restart)
- If the resource change cannot be done in place (e.g., reducing memory below current usage), it falls back to recreating the Pod
You can also resize manually:
# Directly patch a Pod's resource requests without restart
# (strategic merge patch — safe for multi-container pods)
kubectl patch pod myapp-abc123 \
--subresource=resize \
-p '{"spec":{"containers":[{"name":"myapp","resources":{"requests":{"cpu":"200m","memory":"256Mi"}}}]}}'
Do Not Run HPA and VPA Together on the Same Metric
Running HPA (on CPU) and VPA (on CPU) on the same Deployment causes conflicts. They fight over the resource settings.
Safe combinations:
- HPA on CPU → VPA on memory only
- KEDA for horizontal → VPA for vertical right-sizing
- VPA in
Offmode (recommendations only) + HPA for scaling
KEDA — Kubernetes Event-Driven Autoscaling
HPA scales on CPU and memory. But what if you need to scale based on business events — a message queue depth, an HTTP request queue, a database query?
KEDA (Kubernetes Event-Driven Autoscaling) extends HPA to support event-driven scaling from 40+ sources: Kafka, AWS SQS, Azure Service Bus, Redis, HTTP traffic, and more.
KEDA’s biggest feature: scale to zero. When no events are incoming, KEDA scales the Deployment to 0 replicas. When events arrive, it scales back up in seconds. Idle apps cost $0.
Installing KEDA
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
ScaledObject — Scale on Queue Depth
Scale based on a Redis list length:
# keda-redis.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: myapp-keda
spec:
scaleTargetRef:
name: myapp
minReplicaCount: 0 # Scale to zero when queue is empty
maxReplicaCount: 20
pollingInterval: 15 # Check queue every 15 seconds
cooldownPeriod: 60 # Wait 60s before scaling down
triggers:
- type: redis
metadata:
address: redis-service:6379
listName: job-queue
listLength: "5" # 1 replica per 5 items in the queue
kubectl apply -f keda-redis.yaml
Now:
- Queue empty → 0 replicas (free!)
- 5 items → 1 replica
- 50 items → 10 replicas
- 100 items → 20 replicas
Scale on HTTP Traffic
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: myapp-http-scaler
spec:
scaleTargetRef:
name: myapp
minReplicaCount: 1 # Keep 1 replica for HTTP (avoid cold start)
maxReplicaCount: 50
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
metricName: http_requests_total
query: sum(rate(http_requests_total{deployment="myapp"}[1m]))
threshold: "100" # 1 replica per 100 req/s
Cluster Autoscaler
When all your nodes are full, new Pods stay in Pending state. The Cluster Autoscaler adds new nodes to your cluster when needed, and removes them when they are empty.
Cluster Autoscaler integrates with cloud providers:
# AWS EKS example
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
--set cloudProvider=aws \
--set awsRegion=eu-central-1 \
--set autoDiscovery.clusterName=mycluster
On self-hosted clusters (like k3s on a VPS), you would need to provision nodes manually or use a provider’s node API.
Common Mistakes
Not setting resource requests
HPA cannot calculate utilization without resource requests. Always define requests.cpu and requests.memory on every container.
# BAD — HPA cannot work
resources: {}
# GOOD
resources:
requests:
cpu: "100m"
memory: "128Mi"
Setting HPA minReplicas: 0 for critical services
If a service scales to 0, it takes seconds to scale back up when the first request arrives. For user-facing services, set minReplicas: 1 at minimum. Use KEDA’s scale-to-zero only for background workers and batch jobs.
Running HPA and VPA on the same metric
HPA and VPA both try to adjust Pod CPU. They will fight. Use one for horizontal, the other only for a different resource (or in Off mode for recommendations).
What’s Next?
You know how to scale. The final article pulls everything together: a production readiness checklist.
Next: Kubernetes Tutorial #12: Kubernetes Production Checklist