Kubernetes how-to

Kubernetes HPA: Autoscale Pods With autoscaling/v2

The Horizontal Pod Autoscaler changes the replica count of a Deployment, StatefulSet or similar workload based on observed metrics. For CPU and memory it compares live usage with the requests you set on each container. Without metrics-server and with no requests, the autoscaler has nothing to act on, and kubectl shows the target as unknown.

By , Founder at SeaGit

·

Key takeaways

  • Use autoscaling/v2. It accepts several metrics and a behavior section that controls how fast the HPA scales up and down.
  • Resource utilization is measured against requests, not limits. A container with no CPU request gives the HPA no percentage to compare.
  • metrics-server must be running. It serves the metrics.k8s.io API that the HPA reads.
  • When TARGETS shows <unknown>, the HPA cannot read the metric. Check metrics-server, the requests, and the pod readiness before you touch the replica count.

What the HPA needs before it can scale

The HPA controller is built into the control plane. It does not measure anything itself. It reads metrics from the aggregated API layer, and for CPU and memory that is the metrics.k8s.io API, which the metrics-server add-on provides. The official task page is explicit that the cluster administrator must enable the aggregation layer and register that API.

  • metrics-server installed and Running in kube-system, with kubectl top pods returning numbers.
  • CPU or memory requests set on every container in the target pods. Utilization is usage divided by request.
  • A scale target that supports the scale subresource, such as a Deployment or StatefulSet.
bash
# metrics-server must answer before the HPA can work
kubectl get deployment metrics-server -n kube-system
kubectl top pods -n prod

# Requests must be present on the containers the HPA will measure
kubectl get deploy my-api -n prod \
  -o jsonpath='{.spec.template.spec.containers[*].resources.requests}'
kubectl top fails when metrics-server is missing or unhealthy. That is the first thing to fix.

An autoscaling/v2 HPA manifest

The autoscaling/v2 API replaces the older v1 form, which only supported a single CPU target. The example below keeps average CPU at 60 percent across the pods, with a floor of 2 and a ceiling of 10 replicas, and a memory target alongside it. A metric that goes above its target wins, so the HPA scales for whichever resource is tightest.

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-api
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 75
Apply with kubectl apply -f hpa.yaml. Keep the HPA in the same manifest set as the Deployment so the two change together.

Do not set the Deployment replicas field and the HPA target at the same time as two sources of truth. If you apply a Deployment with a fixed replicas value, every apply resets the count the HPA chose. Leave replicas out of the Deployment once the HPA owns the count.

How the replica count is calculated

The controller computes a ratio between the current metric value and the target, then applies a ceiling to the current replica count. The documented formula is desiredReplicas = ceil(currentReplicas times currentMetricValue divided by desiredMetricValue). If the average CPU is 90 percent across 4 pods and the target is 60 percent, the result is ceil(4 times 1.5), which is 6 replicas.

A tolerance of 0.1 by default stops the HPA from acting on small differences. If the ratio is within 10 percent of 1.0, no change is made. When it averages utilization, the controller discards pods that are being deleted or have failed, and it handles pods that are not yet Ready with extra care, so a slow start-up does not trigger an immediate scale-up.

Control the pace with the behavior section

The behavior block tunes scaling in each direction. Its defaults match the historical HPA behavior: scale-down waits for a 300-second stabilization window, and scale-up has no window. The window stops the HPA from removing pods on a short dip in load. Scale-up is immediate so that a spike is answered quickly.

yaml
spec:
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # wait 5 min of lower load before removing pods
      policies:
      - type: Percent
        value: 50                       # remove at most half the pods per period
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Pods
        value: 4                        # add at most 4 pods per 15 seconds
        periodSeconds: 15
      selectPolicy: Max
Only the fields you set are changed. Everything else keeps the default from the HPA algorithm.

Choosing a window

A longer scale-down window protects a service with slow warm-up, such as a JVM or a cache that rebuilds on start. A shorter one reclaims cost faster. Start with the defaults, watch the replica history for a week, and only then change the numbers.

Create and inspect an HPA with kubectl

kubectl autoscale creates an HPA for a Deployment without a file. It is quick for a test, but it only sets a CPU target. Use the YAML when you need memory targets or a behavior section.

bash
# Quick CPU-only HPA, as in the Deployment page examples
kubectl autoscale deployment my-api -n prod --min=2 --max=10 --cpu-percent=60

# List HPAs with current and target metrics
kubectl get hpa -n prod

# Conditions and recent scaling decisions
kubectl describe hpa my-api -n prod
The TARGETS column shows current over target, for example 45%/60%. The describe output has the reason behind each scale action.

The describe output is the best place to look. Its Conditions block reports whether the HPA can compute the metric and whether it is allowed to scale. Its Events list the last scale decisions with the numbers used.

Test the scaling path before you rely on it

An HPA that has never scaled is a guess. Before you rely on it, drive load at the service in a staging namespace and watch three things: the CPU or memory reading, the replica count, and the readiness of the new pods. A healthy result is a steady climb in replicas that stops once the average falls under the target, followed by a slow descent after the scale-down window has passed.

bash
# Watch the HPA decision and the pods together
kubectl get hpa my-api -n staging -w &
kubectl get pods -n staging -l app=my-api -w

# Read the per-pod usage the HPA is using
kubectl top pods -n staging -l app=my-api
Stop the watches when the test ends. Use a load generator that your team already runs, and keep it inside the staging namespace.

Watch for two failure shapes. The first is flapping, where replicas go up and down every minute. That usually means the target is too tight for the real load, or the readiness probe is too slow, so a new pod looks idle while it warms up. The second is no movement at all, which usually means the requests are set too high, so the measured utilization never reaches the target. Fix the requests before you change the target.

Fixing <unknown> in TARGETS

A target that shows <unknown> means the controller could not get a value for the metric. The replica count stays where it is. This is the most common HPA complaint, and it has a short list of causes.

  • metrics-server is not installed, is crash-looping, or cannot reach the kubelets. kubectl top fails in the same way.
  • The containers have no request for the resource. Utilization needs the request as the denominator, so add cpu or memory requests.
  • The pods are not Ready yet. A Deployment that is still starting may show no usable value until its readiness probe passes.
  • The HPA points at the wrong object. A scaleTargetRef with a typo in the name or kind leaves the HPA unable to find pods.
  • A custom or external metric is named in the spec, but no adapter serves it. The API aggregation layer must expose custom.metrics.k8s.io or external.metrics.k8s.io.
bash
# The condition that explains the failure
kubectl get hpa my-api -n prod -o jsonpath='{.status.conditions[*].message}'

# Confirm metrics are flowing at all
kubectl top pods -n prod -l app=my-api
Look for ScalingActive set to False with a reason such as FailedGetResourceMetric. That line names the missing piece.

Frequently asked questions

Kubernetes HPA: Autoscale Pods With autoscaling/v2: FAQ

Why does my HPA show unknown for CPU?

The usual causes are metrics-server not running, containers with no CPU request, or pods that are not Ready. Check kubectl top pods first. If that fails, fix metrics-server. If it works, add the request and confirm the pods become Ready.

Do I need requests if I only use a memory target?

Yes. Memory utilization is also measured as usage divided by the memory request, so the request must be set on the containers, whichever metric you choose.

Should I use autoscaling/v1 or autoscaling/v2?

Use autoscaling/v2 for new manifests. It supports several metrics and the behavior section, which the quick kubectl autoscale form does not set.

Can minReplicas be 0?

For CPU and memory targets, keep minReplicas at 1 or more. Scaling to zero depends on a separate feature gate and on object or external metrics, so check the HPA page before you rely on it. For most services a floor of 2 is the safer default.

Sources

Checked 11 October 2026.

  1. Kubernetes: Horizontal Pod Autoscaling — The algorithm, tolerance, metrics API requirements, the behavior defaults and the autoscaling/v2 examples.
  2. Kubernetes: Deployments, scaling a Deployment — kubectl autoscale on a Deployment and the scaling commands.
  3. Kubernetes: Resource Management for Pods and Containers — Requests and limits, and why utilization is measured against requests.
  4. Kubernetes: kubectl quick reference — kubectl top and related commands used in the checks.

Read next