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).
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
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.
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.
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
kubectlconnected 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-1The 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.yamlThe 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-systemThen confirm it is collecting data:
kubectl top nodes
kubectl top podsIf 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.yamlExample 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: 60Key fields:
scaleTargetRef: must match the exactnameandkindof 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: 300on 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.yamlHow 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.yamlStep 2: Open a watch in a second terminal
kubectl get hpa -wStep 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-generatorREPLICAS will decrease after the 5-minute stabilisation window passes.
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 hpaExample 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.
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-hpaThis 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.
| HPA | Cluster Autoscaler | Karpenter | KEDA | |
|---|---|---|---|---|
| What it scales | Pod replicas | EC2 nodes (node groups) | EC2 nodes (directly) | Pod replicas |
| Trigger | CPU, memory, custom metrics | Unschedulable pods or underused nodes | Unschedulable pods | External events (queues, HTTP, etc.) |
| Scale to zero | No | No | No | Yes |
| Setup on EKS | Built in (needs Metrics Server) | Deploy as add-on | Deploy as add-on | Deploy as operator via Helm |
| Best for | Stateless services scaling on utilisation | Node scaling with standard node groups | Flexible, fast node provisioning | Event-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.
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
Missing resource requests. Without
resources.requests.cpu, HPA cannot calculate utilisation and reports<unknown>in TARGETS. You will see aFailedGetResourceMetricevent when you runkubectl describe hpa. Fix: add CPU requests to every container in the Deployment.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-systemand verifykubectl top podsreturns data.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: 2or higher for any production workload.Setting maxReplicas too low. When HPA hits
maxReplicasduring a real traffic spike, scaling stops and pods become overloaded. SetmaxReplicasto a ceiling that reflects your actual peak load, with some headroom.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.
Expecting HPA to scale nodes. HPA only scales pod replicas. When the cluster runs out of node capacity, pods go
Pending. Check withkubectl get pods —field-selector=status.phase=Pending. The fix is adding Cluster Autoscaler or Karpenter, not adjusting the HPA.Manually scaling a Deployment managed by HPA. Running
kubectl scale deployment web-app —replicas=5will be overridden by HPA within 15 to 30 seconds. HPA ownsspec.replicaswhile active. To manually override, either delete the HPA or setminReplicasandmaxReplicasto 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 podsBest 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.
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.
Summary
- HPA automatically scales pod replica counts based on CPU, memory, or custom metrics. It does not scale nodes.
- EKS does not include Metrics Server by default. Install it as an EKS managed add-on or via the upstream manifest. Verify with
kubectl top pods. - Resource requests (
resources.requests.cpu) are required. Without them, HPA reports<unknown>and never scales. - Use
autoscaling/v2for all new HPA configurations. It supports multiple metrics and fine-grained behaviour tuning. - Scale-up is fast by default. Scale-down has a 5-minute stabilisation window to prevent thrashing.
- HPA scales pods. Cluster Autoscaler and Karpenter scale nodes. You typically need both layers for fully elastic scaling on EKS.
- HPA cannot scale to zero. For scale-to-zero and event-driven autoscaling (SQS, Kafka, HTTP), use KEDA.
- Use
kubectl describe hpaandkubectl top podsas your first diagnostic tools when HPA is not behaving as expected.
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.