Home/Engineer/Docker / Containers/Kubernetes HPA Autoscaling —…
Docker / ContainersAdvancedlinuxkuberneteshpaautoscaling

Kubernetes HPA Autoscaling — Resource-Based Automatic Pod Scaling

A practical guide covering metrics-server installation, HorizontalPodAutoscaler setup with CPU/memory and custom metrics, scaling behavior tuning, and troubleshooting.

Contents
  1. What is HPA (HorizontalPodAutoscaler)?
  2. Installing metrics-server
  3. Prepare the target workload
  4. Create an HPA — CPU-based
  5. How scaling is calculated
  6. Verify with a load test
  7. Tuning scaling behavior
  8. Custom and external metrics (RPS, queue length)
  9. Troubleshooting
  10. Summary

What is HPA (HorizontalPodAutoscaler)?

HPA is a controller that automatically increases or decreases the replica count of a Deployment, ReplicaSet, or StatefulSet based on measured metrics (CPU, memory, custom metrics). When traffic spikes, it adds Pods to keep throughput up; when idle, it scales down to save cost.

  • Horizontal scaling: Adjusts the number of Pods (vs. VPA, which adjusts a Pod's resource limits)
  • The default sync period is 15 seconds; desired replicas are calculated from the ratio of current value to target value
  • A metrics-server (or custom metrics API) that supplies measurements is required

For HPA to work, Pods must have resources.requests set. CPU utilization is calculated as usage / requests, so if requests are missing you get <unknown> and scaling stops.


Installing metrics-server

CPU/memory-based HPA scaling depends on the metrics.k8s.io API provided by metrics-server.

Bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

On test or private clusters, kubelet certificates are often self-signed, so metrics-server cannot connect. Only in that case, temporarily disable TLS verification.

Bash
kubectl patch deployment metrics-server -n kube-system --type=json \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

Verify the installation:

Bash
kubectl top nodes
kubectl top pods
# If values appear, metrics-server is healthy

Prepare the target workload

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  replicas: 1
  selector: { matchLabels: { app: php-apache } }
  template:
    metadata: { labels: { app: php-apache } }
    spec:
      containers:
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports: [{ containerPort: 80 }]
        resources:
          requests:        # Baseline for HPA calculation — required!
            cpu: 200m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
spec:
  selector: { app: php-apache }
  ports: [{ port: 80 }]

Create an HPA — CPU-based

Imperative (simple)

Bash
kubectl autoscale deployment php-apache \
  --cpu-percent=50 --min=1 --max=10

The v2 API supports multiple metrics and scaling behavior tuning.

YAML
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization        # Percentage relative to requests
        averageUtilization: 50
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue       # Absolute value
        averageValue: 200Mi

When multiple metrics are present, each is calculated and the largest replica count is chosen (conservative scale-up).


How scaling is calculated

Desired replicas are determined by the following formula:

CODE
desiredReplicas = ceil( currentReplicas * (currentMetric / targetMetric) )

Example: 3 current Pods, average CPU utilization 90%, target 50% → ceil(3 * 90/50) = ceil(5.4) = 6 replicas.

Near the target (default ±10% tolerance), scaling is skipped to prevent flapping.


Verify with a load test

Bash
# Load generator
kubectl run -it --rm load --image=busybox:1.36 --restart=Never -- \
  /bin/sh -c "while true; do wget -q -O- http://php-apache; done"

# Watch in another terminal in real time
kubectl get hpa php-apache --watch
# NAME         TARGETS         MINPODS  MAXPODS  REPLICAS
# php-apache   250%/50%        1        10       1 → 5 → ...

After the load stops, it waits the default 5 minutes (stabilizationWindowSeconds) and then scales down gradually.


Tuning scaling behavior

To scale up quickly under sudden traffic and scale down cautiously, adjust behavior.

YAML
spec:
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0     # Scale up immediately
      policies:
      - type: Percent
        value: 100                       # Increase by at most 100% at a time
        periodSeconds: 30
      - type: Pods
        value: 4                         # Or at most 4 Pods
        periodSeconds: 30
      selectPolicy: Max                  # Choose the more aggressive of the two policies
    scaleDown:
      stabilizationWindowSeconds: 300    # Scale down after 5 minutes of stabilization
      policies:
      - type: Percent
        value: 10                        # Decrease by at most 10% at a time
        periodSeconds: 60
ParameterMeaning
stabilizationWindowSecondsHistorical window used for the decision (prevents flapping)
policy PercentRatio-based change limit
policy PodsAbsolute count change limit
selectPolicyMax (aggressive) / Min (conservative) / Disabled

Custom and external metrics (RPS, queue length)

When CPU is not enough, use Prometheus Adapter or KEDA to scale on application metrics.

YAML
  metrics:
  - type: Pods                          # Per-Pod custom metric
    pods:
      metric: { name: http_requests_per_second }
      target:
        type: AverageValue
        averageValue: "100"
  - type: External                      # External metric (e.g. SQS queue length)
    external:
      metric: { name: sqs_queue_length }
      target:
        type: AverageValue
        averageValue: "30"

For event/message-driven workloads, KEDA is more convenient than plain HPA: it supports scale-to-zero and many scalers (Kafka, RabbitMQ, Cron, and more).


Troubleshooting

Bash
kubectl describe hpa php-apache       # Check events and Conditions
SymptomCause / Fix
TARGETS shows <unknown>metrics-server not running, or requests missing
FailedGetResourceMetricmetrics-server certificate or network issue
Not scaling upmaxReplicas reached, or within tolerance
Slow scale-downLong scaleDown stabilizationWindow (expected)
FlappingIncrease the stabilizationWindow

HPA and Cluster Autoscaler should be used together. If HPA adds Pods but nodes lack capacity, Pods stay Pending—so Cluster Autoscaler (or Karpenter) must add nodes automatically.


Summary

ItemKey point
Prerequisitesmetrics-server + Pod resources.requests
API versionautoscaling/v2 (multiple metrics and behavior)
Formulaceil(current * currentValue/targetValue)
Metric typesResource (CPU/Mem) / Pods / Object / External
behaviorAggressive scaleUp + conservative scaleDown recommended
Event workloadsConsider KEDA (scale-to-zero)
When nodes are insufficientPair with Cluster Autoscaler/Karpenter

The core of HPA is ratio calculation against requests. Set realistic requests, tune scaleUp to be fast and scaleDown to be slow, and also configure a node autoscaler so you can handle real traffic swings reliably.

#kubernetes#hpa#autoscaling#metrics-server#scaling
Editorial note

AI tools may assist drafting. Check the evidence in each guide for whether commands were run and which environment was used. Confirm the current docs for your version before applying instructions, and email us about errors. email us

Topic-related official docs (auto-linked · not per-claim sources)Kubernetes documentation ↗Docker documentation ↗

Questions & answers

Ask a question about this guide. We'll review and reply.