Kubernetes ships with an autoscaler. Many teams install a second one. The confusion starts when both exist and nobody can say which workload belongs to which. HPA and KEDA are not competitors; they answer different questions. HPA asks "how busy are my pods?" KEDA asks "how much work is waiting?" Picking wrong means either idle pods burning money or a queue growing while your cluster insists everything is fine.
HPA: scaling on resource pressure
The Horizontal Pod Autoscaler is built into Kubernetes. It watches metrics, usually CPU and memory utilization, and adjusts replica counts to keep those metrics near a target. A typical configuration looks like this:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
HPA is the right tool when load correlates with resource usage. A web API under traffic pressure burns CPU, so CPU utilization is a faithful proxy for demand. The autoscaler reacts, adds pods, and utilization falls back to target. It is simple, built in, and well understood by every Kubernetes operator, which is a real advantage at 3 AM.
HPA has two limits worth knowing. First, it cannot scale to zero. Minimum replicas is at least one, so an idle service still costs at least one pod forever. Second, it reacts to symptoms, not causes. If your workers are stuck waiting on a slow database, CPU stays low and HPA sees nothing wrong while your queue grows. Resource metrics describe the pods, not the work.
KEDA: scaling on waiting work
KEDA, the Kubernetes Event-Driven Autoscaler, scales on external signals: queue depth, message lag, database rows, cron schedules, HTTP request rates. Instead of asking how busy the pods are, it asks how much work is unclaimed, and it sizes the fleet to the backlog:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: orders-scaler
spec:
scaleTargetRef:
name: orders-worker
minReplicaCount: 0
maxReplicaCount: 30
triggers:
- type: kafka
metadata:
bootstrapServers: kafka:9092
consumerGroup: orders
topic: orders
lagThreshold: "100"
The two properties that matter are scale to zero and backlog awareness. A worker that processes a queue nobody is feeding should cost nothing, and KEDA delivers that: zero messages, zero pods, zero bill. And when a burst arrives, KEDA scales on the lag itself, not on the CPU of pods that have not started yet. This is the correct signal for event-driven workloads, because the queue is the demand.
KEDA's cost is complexity. It is an additional component with its own triggers, its own failure modes, and its own scaling quirks. A misconfigured trigger can scale you to the maximum during a poison-message storm, or flap replicas up and down as lag oscillates around the threshold. KEDA also needs a metric source for every trigger, which means your queues and stores must expose the numbers it scales on. That instrumentation work is real and should be budgeted.
The decision rule
Use HPA when demand shows up as resource pressure: request-driven services, APIs, web frontends, anything where traffic and CPU move together. The built-in tool is simpler, and simpler is better when it is sufficient.
Use KEDA when demand shows up as waiting work: queue consumers, event processors, batch jobs, cron-driven workloads, anything where the backlog is the truth and CPU is a lagging indicator. The scale-to-zero property alone often pays for the added complexity on sporadic workloads.
Use both when the system has both shapes, which is common. An API deployment on HPA feeding a queue consumed by workers on KEDA is a standard, healthy topology. They do not conflict because they scale different deployments on different signals.
Three operational notes apply to either choice. First, set max replicas from cost, not optimism: an autoscaler without a ceiling is a billing incident waiting for a traffic spike. Second, test scaling behavior in staging with real load, not by guessing thresholds: watch for flapping, slow scale-up, and cold-start latency on the first pod. Third, alert on the scaling signal itself. If replicas sit at maximum for an hour, that is not success, that is a capacity or downstream problem wearing an autoscaler's clothes.
The autoscaler debate is usually a symptom of a missing question: what is the demand signal? Answer that and the tool picks itself. CPU pressure means HPA. Waiting work means KEDA. Everything else is configuration.
Misconfigurations that cause the most pages
The most common HPA mistake is targeting average CPU across pods when the workload is bursty. Average utilization hides the single pod that is melting: ten pods at 10 percent and one pod at 95 percent average to a calm number while one user population suffers. For latency-sensitive services, target a percentile-based custom metric or set the target low enough that headroom exists before saturation. The autoscaler can only react to what it can see, and averages lie.
The most common KEDA mistake is a lag threshold set without knowing normal lag. A queue that normally sits at 200 messages with a threshold of 100 keeps the system permanently scaled up, which is just a fixed fleet with extra steps. Measure normal lag for a week before setting the threshold, then set it above the normal peak with room for bursts. And set a cooldown period that matches your workload: too short and replicas flap, too long and you pay for idle pods after every burst.
A third mistake applies to both: scaling the wrong thing. If the bottleneck is the database, adding application pods makes the problem worse, because more pods mean more connections and more contention. Before tuning any autoscaler, confirm that the tier you are scaling is actually the bottleneck. Autoscaling a tier that is waiting on another tier is the infrastructure equivalent of pressing the accelerator with the parking brake on.
Testing before you trust it
An autoscaler you have not tested is a hope, not a control. Load-test in staging with a realistic traffic shape: ramp up over minutes, hold, spike, and drop. Watch three things. Time to first new pod, which tells you whether scale-up is fast enough to matter. Stability at the plateau, which tells you whether thresholds flap. And time to scale down, which tells you what the burst actually costs. Run the same test twice, because the second run reveals caching effects the first run hid. Write the numbers down. "It scales" is not a test result; "ten to thirty pods in ninety seconds, stable within five percent" is.
One page to keep on the wall
If your team keeps one autoscaling note, make it this. HPA scales on resource pressure and never to zero. KEDA scales on waiting work and does reach zero. Every workload maps to one of those sentences, and the mapping decides the tool. When someone proposes adding the other autoscaler, ask which sentence their workload matches. If the answer is vague, the workload is not understood well enough to autoscale yet, and the next step is measurement, not configuration. Autoscaling rewards teams that know their demand signal. It punishes teams that guess it, with a bill.
Top comments (0)