Horizontal Pod Autoscaling on AWS EKS: Setup, YAML Example, and Common Mistakes

Horizontal Pod Autoscaling (HPA) is a built-in Kubernetes feature that automatically adjusts the number of pod replicas in a Deployment based on observed metrics. When CPU climbs, HPA adds pods. When load drops, it removes them. On EKS, HPA works out of the box — but it needs Metrics Server installed separately, and it will silently do nothing if your containers are missing resource requests. This guide covers how HPA works, how to set it up on EKS, how to test it, and how to diagnose it when something goes wrong.

What Horizontal Pod Autoscaling is

Horizontal Pod Autoscaling is a Kubernetes controller that watches a Deployment (or StatefulSet, or ReplicaSet) and automatically adjusts the running pod replica count based on observed resource usage.

The word “horizontal” is key. It means adding or removing identical copies of your pod, not making any single pod bigger. Adding more pods is horizontal scaling. Changing the CPU or memory limits on existing pods would be vertical scaling, and that is handled by a different component (the Vertical Pod Autoscaler, which is outside the scope of this guide).

What HPA does not do

HPA does not scale EC2 nodes. If HPA creates more pods than the cluster has capacity to run, those pods sit in Pending state. Node scaling is handled separately by Cluster Autoscaler, Karpenter, or EKS Auto Mode. Both layers work together but neither one handles the other’s job.

HPA in simple terms

Analogy

Think of a supermarket during a Saturday rush. When queues get long, the manager opens more checkout lanes. When the store quiets down, some cashiers go on break. The manager does not guess in advance how many lanes to open. They respond to what they can actually see: how long the queues are right now.

HPA works the same way. Instead of checkout lanes, it manages pod replicas. Instead of queue length, it watches CPU utilisation. Instead of a manager making calls every hour, it checks automatically every 15 seconds.

You set a CPU target, say 70%. You set a floor (minReplicas) and a ceiling (maxReplicas). HPA does the rest. Your application gets more capacity when it needs it and stops paying for idle capacity when it does not.

How HPA works on EKS

HPA runs a continuous reconciliation loop inside the Kubernetes control plane. Every 15 seconds it runs through four steps.

Step 1: Collect metrics

HPA queries Metrics Server for current CPU and memory usage across all pods in the target Deployment.

Step 2: Calculate utilisation

HPA expresses CPU usage as a percentage of each pod’s resources.requests.cpu. If a pod requests 250m CPU and is currently using 175m, utilisation is 70%. Resource requests are the denominator in this calculation. Without them, the denominator is zero and HPA cannot compute anything.

Step 3: Calculate desired replicas

HPA uses this formula:

desiredReplicas = ceil( currentReplicas x (currentUtilisation / targetUtilisation) )

Example: 3 pods at 84% average CPU with a 70% target gives ceil(3 x (84 / 70)) = ceil(3.6) = 4. HPA scales to 4 pods.

Step 4: Update the Deployment

If the desired count is within your minReplicas and maxReplicas bounds and differs from the current count, HPA updates spec.replicas on the Deployment. Kubernetes creates or removes pods to match.

Mental model

HPA is like a thermostat. You set a target temperature of 70% CPU. The thermostat checks the reading every 15 seconds. Too hot? It spins up more pods. Too cool for long enough? It switches some off. It never goes below your minimum or above your maximum. You stop manually adjusting the dial.

Scale-up is aggressive by default. When utilisation exceeds the target, HPA acts on the next cycle.

Scale-down is deliberately slow. After utilisation drops below the target, HPA waits 5 minutes before removing any pods. This prevents thrashing: a cycle where the HPA scales down, traffic bounces back, and it has to scale straight back up again. The 5-minute window is intentional protection, not a limitation.

When to use HPA

HPA is the right tool when your workload is stateless and receives variable traffic. Strong fits include:

  • Stateless web applications. HTTP services where any pod can handle any request. HPA was built for this pattern.
  • REST and gRPC API backends. Services with variable request rates across the day can scale up during peak hours and back down overnight.
  • Worker services consuming queues. SQS consumers can scale on queue depth via KEDA, keeping processing latency stable under variable load.
  • Services with predictable traffic patterns. Even when patterns are predictable, HPA handles the timing automatically without cron-based scaling scripts.

When HPA is the wrong tool

  • Stateful workloads. Databases and stateful services often cannot add replicas without coordinating data access. Scaling a StatefulSet with HPA is possible in theory but requires careful thought about state management and is rarely the right first approach.
  • Workloads with no meaningful metrics. If pods have no resource requests and you have no useful custom metrics, HPA has nothing to act on.
  • Scale-to-zero requirements. HPA cannot scale below minReplicas: 1. If you need a service to run zero pods when idle, use KEDA instead.
Slow-starting workloads

If your pods take several minutes to start and accept traffic, HPA may not add capacity quickly enough during a fast spike. For slow-starting services, a higher minReplicas is often more effective than aggressive autoscaling. Keeping warm replicas running is cheaper than an outage while new pods come up.

Prerequisites

Before configuring HPA, confirm you have:

  • A running EKS cluster with node groups or Fargate profiles configured
  • kubectl connected to the cluster (see Deploying Containers with kubectl)
  • Resource requests (resources.requests.cpu) set on every container you want to scale
  • Metrics Server installed and returning data (covered next)

Install Metrics Server on EKS

HPA needs a metrics source to function. For CPU and memory-based scaling it uses Metrics Server, a lightweight cluster add-on that collects resource usage from each node’s kubelet. EKS does not ship with Metrics Server pre-installed. You have two installation options.

Option 1: EKS managed add-on (recommended)

Install Metrics Server as an EKS managed add-on through the AWS console or with eksctl:

eksctl create addon \
  --name metrics-server \
  --cluster my-cluster \
  --region us-east-1

The managed add-on path means AWS handles version compatibility and updates it alongside your cluster during version upgrades. See Upgrading EKS Clusters for how add-on management fits into that process.

Option 2: Manifest install

The standard upstream manifest works on EC2 node groups:

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

The default Metrics Server configuration does not work on Fargate nodes. Fargate does not allow direct kubelet communication on the standard port. For Fargate workloads, run Metrics Server on EC2 nodes in your cluster, or patch the Deployment to add the —kubelet-insecure-tls flag alongside the appropriate VPC networking configuration.

Verify Metrics Server is working

kubectl get deployment metrics-server -n kube-system

Then confirm it is collecting data:

kubectl top nodes
kubectl top pods
Pass/fail check

If kubectl top pods returns real numbers, Metrics Server is working and HPA can function. If it returns an error or shows no data, stop here and fix the Metrics Server installation before going further. HPA will silently fail to scale if this step is skipped.

Example Deployment with resource requests

Before creating an HPA, your Deployment must have resource requests set. Without resources.requests.cpu, HPA cannot calculate utilisation and will show <unknown> in the TARGETS column indefinitely.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: nginx:latest
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "200m"       # Required for CPU-based HPA
              memory: "256Mi"   # Required for memory-based HPA
            limits:
              cpu: "500m"
              memory: "512Mi"

requests define the baseline HPA divides against to produce a percentage. limits cap what a single pod can consume. Set both on every container in every Deployment that HPA will target.

kubectl apply -f deployment.yaml

Example HPA YAML

Use the autoscaling/v2 API. It is the current stable version and supports multiple metrics and the behavior block for tuning scale-up and scale-down speed. The older autoscaling/v1 only supports CPU and should be avoided for new configurations.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60

Key fields:

  • scaleTargetRef: must match the exact name and kind of your Deployment.
  • minReplicas: 2: never scale below 2 pods. Setting this to 1 means a single pod crash takes your service down while HPA scales back up. Use at least 2 in production.
  • maxReplicas: 10: never scale above 10. Set this based on your actual peak load, not a guess.
  • averageUtilization: 70: scale up when average CPU across all pods exceeds 70% of their request. A reasonable starting point for most stateless services.
  • stabilizationWindowSeconds: 300 on scale-down: wait 5 minutes of sustained below-target usage before removing pods.
  • scaleDown.policies: remove at most 2 pods per 60 seconds, keeping scale-down gradual.
kubectl apply -f hpa.yaml

How to test HPA

The most reliable way to validate HPA is to generate real CPU load and observe the response.

Step 1: Apply both manifests

kubectl apply -f deployment.yaml
kubectl apply -f hpa.yaml

Step 2: Open a watch in a second terminal

kubectl get hpa -w

Step 3: Generate load

kubectl run load-generator \
  --image=busybox:1.28 \
  --restart=Never \
  -- /bin/sh -c "while true; do wget -q -O- http://web-app; done"

Step 4: Observe scaling

You should see TARGETS climb above 70%, then REPLICAS increase as HPA responds. The first scale-up typically takes 1 to 2 minutes. If new pods stay in Pending, your cluster has no remaining node capacity. You need Cluster Autoscaler or Karpenter to handle node scaling, as described in the comparison section below.

Step 5: Stop the load and watch scale-down

kubectl delete pod load-generator

REPLICAS will decrease after the 5-minute stabilisation window passes.

What a healthy test looks like

TARGETS rises above your threshold, REPLICAS increases within 2 minutes, pods reach Running state, and TARGETS gradually falls back to near your target percentage. If TARGETS shows <unknown> at any point during the test, stop and fix the Metrics Server or resource requests issue before continuing.

How to read HPA output

kubectl get hpa

Example output:

NAME           REFERENCE              TARGETS     MINPODS   MAXPODS   REPLICAS   AGE
web-app-hpa    Deployment/web-app     45%/70%     2         10        3          8m
  • TARGETS: current/target. 45%/70% means average CPU is at 45% against a target of 70%. HPA is satisfied and not scaling.
  • MINPODS / MAXPODS: the bounds you configured.
  • REPLICAS: how many pods are currently running.
When TARGETS shows <unknown>

This almost always means one of two things: resource requests are missing from the container spec, or Metrics Server is not running. Check kubectl describe hpa web-app-hpa for the FailedGetResourceMetric event, then run kubectl top pods to confirm Metrics Server is working. Fix one or both before continuing.

For a full picture including recent scaling decisions and the metric values HPA used:

kubectl describe hpa web-app-hpa

This is your primary diagnostic tool when scaling is not behaving as expected.

HPA vs Cluster Autoscaler vs Karpenter vs KEDA

These four systems are commonly mentioned together on EKS. They are not alternatives to each other. They operate at different levels.

HPACluster AutoscalerKarpenterKEDA
What it scalesPod replicasEC2 nodes (node groups)EC2 nodes (directly)Pod replicas
TriggerCPU, memory, custom metricsUnschedulable pods or underused nodesUnschedulable podsExternal events (queues, HTTP, etc.)
Scale to zeroNoNoNoYes
Setup on EKSBuilt in (needs Metrics Server)Deploy as add-onDeploy as add-onDeploy as operator via Helm
Best forStateless services scaling on utilisationNode scaling with standard node groupsFlexible, fast node provisioningEvent-driven workloads, batch, scale-to-zero

The distinction that trips people up most often: HPA and KEDA scale pods. Cluster Autoscaler and Karpenter scale nodes. If HPA adds more pods than the cluster can schedule, those pods sit Pending until nodes are added. Both layers are usually needed.

On EKS Auto Mode

If you are using EKS Auto Mode, AWS manages node scaling automatically. You configure HPA and node capacity is handled without needing to deploy or configure Cluster Autoscaler or Karpenter separately.

Common mistakes and troubleshooting

  1. Missing resource requests. Without resources.requests.cpu, HPA cannot calculate utilisation and reports <unknown> in TARGETS. You will see a FailedGetResourceMetric event when you run kubectl describe hpa. Fix: add CPU requests to every container in the Deployment.

  2. Metrics Server not installed. HPA is built into Kubernetes. Metrics Server is not. The HPA resource creates successfully without Metrics Server, which makes this easy to miss. Confirm with kubectl get deployment metrics-server -n kube-system and verify kubectl top pods returns data.

  3. Setting minReplicas to 1. A single pod means zero redundancy. If that pod or its node fails while HPA is in the middle of scaling, the service goes down. Use minReplicas: 2 or higher for any production workload.

  4. Setting maxReplicas too low. When HPA hits maxReplicas during a real traffic spike, scaling stops and pods become overloaded. Set maxReplicas to a ceiling that reflects your actual peak load, with some headroom.

  5. Using CPU for the wrong workload. CPU is a reliable signal for request-driven services. For I/O-heavy or queue-based workloads, CPU can stay low even under heavy load. Use custom metrics via KEDA for workloads where CPU is not meaningful.

  6. Expecting HPA to scale nodes. HPA only scales pod replicas. When the cluster runs out of node capacity, pods go Pending. Check with kubectl get pods —field-selector=status.phase=Pending. The fix is adding Cluster Autoscaler or Karpenter, not adjusting the HPA.

  7. Manually scaling a Deployment managed by HPA. Running kubectl scale deployment web-app —replicas=5 will be overridden by HPA within 15 to 30 seconds. HPA owns spec.replicas while active. To manually override, either delete the HPA or set minReplicas and maxReplicas to the same value.

Useful troubleshooting commands:

kubectl describe hpa web-app-hpa                              # Full event log and metric values
kubectl top pods                                              # Confirm Metrics Server is returning data
kubectl top nodes                                             # Node-level resource usage
kubectl get deployment metrics-server -n kube-system         # Verify Metrics Server is running
kubectl get pods --field-selector=status.phase=Pending       # Check for unscheduled pods

Best practices

  • Start with CPU at 70%. This is a sensible default for most stateless services. Adjust once you can observe how your specific application responds under real load.
  • Set minReplicas to at least 2. Ensures basic availability even if a single pod or node fails.
  • Set maxReplicas to a realistic ceiling. Base this on actual peak traffic with headroom, not a round number you picked at random.
  • Load test before going to production. Run a load test, watch kubectl get hpa -w, and verify that HPA scales fast enough and that nodes have capacity. See Monitoring EKS Clusters for how to observe this at scale.
  • Monitor latency alongside CPU. CPU is a proxy signal. Set up latency alerting so you catch cases where CPU looks fine but users are experiencing slow responses.
  • Pair HPA with a node autoscaler. HPA without Cluster Autoscaler or Karpenter only works until your current nodes are full. On EKS managed node groups, configure one or the other.
  • Keep resource requests accurate. Padded or guessed requests make HPA calculations unreliable. Profile your application under realistic load to get numbers you can actually use.
  • Log scaling events. HPA scaling actions appear in Kubernetes events. Forward them to your logging system (see Logging in Kubernetes on AWS) to make post-incident analysis easier.
If you are setting up HPA for the first time

Start with CPU only at 70%, set minReplicas: 2 and a generous maxReplicas, then run a load test. Watch kubectl get hpa -w while the test runs. Tune the target percentage and replica bounds from what you actually observe. Do not tune by guessing.

Frequently asked questions

What is the difference between HPA and Cluster Autoscaler?

HPA scales the number of pods inside your cluster based on CPU, memory, or custom metrics. Cluster Autoscaler scales the number of EC2 nodes in your node groups when pods cannot be scheduled because there is not enough capacity. They work together: HPA requests more pods, Cluster Autoscaler provisions the nodes to run them. On EKS you also have Karpenter as a more flexible node autoscaler alternative.

Why does HPA show unknown CPU usage?

The most common cause is missing resource requests on the container. HPA calculates CPU utilisation as current usage divided by the requested CPU. If resources.requests.cpu is not set on a container, HPA has no denominator and reports unknown. Set CPU requests on every container in the Deployment and the HPA will start reporting real values within about a minute.

Can HPA scale to zero replicas?

No. The standard Kubernetes HPA has a minimum of 1 replica. If you need scale-to-zero behaviour, for example a worker that should only run when messages are in an SQS queue, use KEDA (Kubernetes Event-Driven Autoscaling). KEDA extends HPA and supports true zero-replica scaling based on external events.

Should I use CPU metrics or custom metrics for HPA?

CPU is the right starting point for most stateless web services and APIs. It is simple, requires only Metrics Server, and responds well to request-driven load. Custom metrics via KEDA or a Prometheus adapter are better for workloads where CPU is not a reliable signal, for example SQS consumers where queue depth is a more accurate indicator of load than CPU utilisation.

Do I need Karpenter or Cluster Autoscaler in addition to HPA?

If your cluster uses managed node groups, yes. You need either Cluster Autoscaler or Karpenter to scale nodes when HPA adds more pods than the cluster can currently schedule. Without a node autoscaler, new pods will stay in Pending state. If you are using EKS Auto Mode or EKS on Fargate, node capacity is managed automatically and you do not need to configure either separately.

Last verified: 4 April 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.