Engineering Notes
Home/DevOps & Cloud/Kubernetes/Autoscaling in Kubernetes
DevOps & Cloud · Kubernetes

Autoscaling in Kubernetes

Scale pods with the HPA or KEDA, right-size them with the VPA, and add nodes with Karpenter or Cluster Autoscaler. Compute HPA decisions yourself, then watch the layers cooperate during a traffic spike.

☁️ DevOps & Cloud● Advanced⏱ 4 min read▶ 2 interactive🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Three layers: pods horizontally, pods vertically, nodes
  • HPA math, metrics sources and behaviour tuning
  • KEDA for event-driven scaling and scale to zero
  • Karpenter vs Cluster Autoscaler, PDBs and headroom

1Three Layers of Autoscaling

LayerWhat scalesTools
Pods horizontallynumber of replicasHorizontalPodAutoscaler (HPA), KEDA
Pods verticallyrequests/limits of each PodVerticalPodAutoscaler (VPA), in-place resize
Nodesnumber/size of machinesCluster Autoscaler, Karpenter

They work together: HPA adds Pods → some are Pending for lack of capacity → the node autoscaler adds nodes → Pods schedule. When load drops, HPA removes Pods → nodes become underused → the node autoscaler consolidates and removes them.

2HorizontalPodAutoscaler

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: api }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: api }
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 65 }   # % of *requests*
    - type: Pods
      pods:
        metric: { name: http_requests_per_second }               # via Prometheus Adapter
        target: { type: AverageValue, averageValue: "50" }
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies: [{ type: Percent, value: 100, periodSeconds: 30 }]   # at most double every 30 s
    scaleDown:
      stabilizationWindowSeconds: 300                                # wait 5 min before shrinking
      policies: [{ type: Pods, value: 2, periodSeconds: 60 }]
  • Formula: desired = ceil(current × currentMetric / target); with multiple metrics, the highest wins.
  • Needs metrics-server for CPU/memory; custom/external metrics via Prometheus Adapter or KEDA.
  • CPU utilization is relative to requests — wrong requests mean wrong scaling.
  • Memory is a poor HPA signal for most runtimes (memory rarely drops after load).
  • Don't set replicas in the Deployment manifest when an HPA manages it (GitOps will fight the HPA).

3KEDA: Event-Driven Autoscaling

KEDA scales on external event sources — queue length (SQS, RabbitMQ, Kafka lag), cron schedules, Prometheus queries — and can scale to zero:

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: thumbnail-worker }
spec:
  scaleTargetRef: { name: thumbnail-worker }
  minReplicaCount: 0
  maxReplicaCount: 50
  cooldownPeriod: 120
  triggers:
    - type: aws-sqs-queue
      metadata: { queueURL: "https://sqs.eu-west-1.amazonaws.com/111111111111/thumbnails", queueLength: "20", awsRegion: eu-west-1 }
      authenticationRef: { name: keda-aws-pod-identity }

Queue-based scaling targets the cause (backlog) rather than the symptom (CPU) — ideal for workers.

4VerticalPodAutoscaler

VPA observes actual usage and recommends (or applies) requests. Modes: Off (recommendations only — great for right-sizing), Initial (set at Pod creation), Recreate/InPlaceOrRecreate (update running Pods). Don't let VPA and HPA both act on CPU for the same workload.

5Node Autoscaling

  • Cluster Autoscaler: scales predefined node groups (ASGs) up when Pods are Pending and down when nodes are underutilized and their Pods can move elsewhere.
  • Karpenter: provisions right-sized nodes directly from Pending Pods' requirements (instance types, arch, Spot/on-demand, zones) in about a minute, and consolidates continuously by replacing/removing underused nodes.
yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: default }
spec:
  template:
    spec:
      requirements:
        - { key: karpenter.sh/capacity-type, operator: In, values: [spot, on-demand] }
        - { key: kubernetes.io/arch, operator: In, values: [arm64, amd64] }
      nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: default }
  limits: { cpu: "400" }
  disruption: { consolidationPolicy: WhenEmptyOrUnderutilized, consolidateAfter: 1m }

Protect workloads during scale-down with PodDisruptionBudgets and graceful shutdown; mark Pods that must not move with karpenter.sh/do-not-disrupt.

Under the HoodThe HPA Control Loop▾

Every 15 seconds the HPA controller (in kube-controller-manager) reads metrics for the target's Pods from the metrics APIs (metrics.k8s.io from metrics-server, custom.metrics.k8s.io/external.metrics.k8s.io from adapters such as Prometheus Adapter or KEDA's metrics server). For resource metrics it averages usage over Pods as a percentage of their requests, ignoring Pods that are not yet ready or are missing metrics (conservatively assuming 0% on scale-up / 100% on scale-down for those), and skips scaling if the ratio is within a 10% tolerance. It computes a recommendation per metric, takes the maximum, applies behavior policies and the stabilization window (scale-down uses the highest recommendation over the last 5 minutes to prevent flapping), and updates the scale subresource of the Deployment. New Pods then need to be scheduled — if no node fits, they're Pending until Karpenter or Cluster Autoscaler adds capacity, which is why fast-booting nodes, pre-pulled images and some headroom (overprovisioning with low-priority placeholder Pods) matter for spiky traffic.

Rising load raises Pod CPU metrics; the HPA reads them from metrics-server or KEDA and increases replicas; Pods that don't fit stay Pending until Karpenter or Cluster Autoscaler launches nodes; VPA right-sizes requests, and on low load consolidation removes nodes
Rising load raises Pod CPU metrics; the HPA reads them from metrics-server or KEDA and increases replicas; Pods that don't fit stay Pending until Karpenter or Cluster Autoscaler launches nodes; VPA right-sizes requests, and on low load consolidation removes nodes

⚠️ Common Pitfalls

  1. No or wrong requests — HPA percentages and bin-packing are meaningless.
  2. HPA on memory for runtimes that never release memory.
  3. replicas in Git fighting the HPA.
  4. Scale-down too aggressive — flapping; no PDBs during consolidation.
  5. Slow startup (big images, long JVM warm-up) making autoscaling too late; use startup probes, pre-scaling or KEDA cron triggers.
  6. No maxReplicas/node limits — a bug or attack scales your bill.

📋 Quick Reference

text
HPA autoscaling/v2: CPU % of requests · custom/external metrics · behavior (stabilization, policies) · max of metrics
KEDA: event sources (SQS, Kafka lag, cron, Prometheus) · scale to zero
VPA: Off (recommend) / Initial / Recreate · not with HPA on same resource
nodes: Cluster Autoscaler (node groups) · Karpenter (NodePools, Spot, consolidation) · PDBs

🧠 Key takeaways

  • Pods scale horizontally (HPA/KEDA) and vertically (VPA); nodes scale with Karpenter or Cluster Autoscaler.
  • HPA: desired = ceil(current × metric/target) on % of requests, so wrong requests mean wrong scaling; tune behavior.
  • KEDA scales on queue length/lag and can scale to zero; don't combine HPA and VPA on the same CPU metric.
  • Keep headroom, use PDBs, set max replicas and node limits, and alert when the HPA sits at max.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What autoscalers does Kubernetes have?core▾

HPA scales pod replicas on metrics (CPU, memory, custom/external). VPA adjusts pod resource requests. Cluster Autoscaler/Karpenter adds or removes nodes based on pending pods and utilization. KEDA scales on event sources (queue length), including to zero.

Follow-ups interviewers ask
  • Can HPA and VPA scale on CPU together? Not on the same metric; they conflict.
Q2Why must pods have resource requests for HPA?core▾

CPU utilization targets are a percentage of requests; without requests, HPA can't compute utilization. Requests also drive scheduling and node autoscaling.

Follow-ups interviewers ask
  • Requests vs limits? Requests reserve capacity; limits cap usage (CPU throttling, memory OOM-kill).
Q3Which metric should you scale on?core▾

The one that reflects load and saturation: requests per second or concurrency for web services, queue depth/lag for workers, CPU only if it correlates. Custom metrics come via Prometheus Adapter or KEDA.

Follow-ups interviewers ask
  • Why is CPU often misleading? I/O-bound services can be saturated at low CPU.
Q4How do you avoid scaling flapping and slow reactions?senior▾

Tune HPA behaviour (stabilization windows, scale-up/down policies), keep pods fast to start, over-provision with low-priority placeholder pods for node headroom, and use predictive or scheduled scaling for known peaks.

Follow-ups interviewers ask
  • What are placeholder pods? Low-priority pods that reserve capacity and get evicted for real workloads.
Q5How do you protect availability during scale-down and node drains?core▾

PodDisruptionBudgets limit simultaneous voluntary disruptions, graceful termination handles SIGTERM, and topology spread constraints keep replicas across nodes and zones.

Follow-ups interviewers ask
  • What's a PDB? e.g. minAvailable: 2 ensures at least two pods remain during drains.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. 4 replicas at 130% CPU with a 65% target → desired?
ceil(4 × 130/65) = 8.
2. CPU utilization in the HPA is measured relative to…
That's why requests must be right.
3. Which scales a queue worker to zero when the queue is empty?
Event-driven scaling.
4. New pods are Pending for lack of capacity. What adds nodes?
Node autoscaling reacts to unschedulable pods.