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

TypeWhat scalesTool
HorizontalNumber of PodsHPA, KEDA
VerticalCPU/RAM per PodVPA
ClusterNumber of nodesCluster 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 Off mode (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