HPA, VPA, KEDA, and Karpenter give five contradictory answers to the same sizing problem
On a single Kubernetes deployment, four autoscaling controllers can demand 3, 11, or 24 replicas at the same time, each acting on one isolated variable. The four collisions that result are not fixed with a fifth controller, but by treating sizing as a single, coupled equation.
October 24, 2026. A post on The New Stack walks through a case study: a checkout-api service declared in Helm with replicas: 3, cpu: 1000m, and memory: 2Gi. The live cluster tells a different story: HPA scaled it to 11 replicas on CPU load, VPA recommends dropping the request to 350m, KEDA demands 24 replicas after a Kafka lag spike, and Karpenter provisions two new nodes. Five sources, five contradictory answers.
For a platform team, the diagnosis fits in one sentence: each controller acts logically in isolation, and none of them sees the full picture. That is an architectural failure, not a configuration bug.
Unix philosophy pushed past its logic
Kubernetes was built on the Unix principle: small, modular tools that do one thing well. grep and awk compose cleanly because they process immutable text streams sequentially — one finishes, then hands its output to the next.
Autoscaling controllers do not work that way. They run concurrently, on the same workload, and mutate the same API fields: .spec.replicas, .resources.requests, and the underlying node pool. Each runs an independent reconciliation loop and assumes it holds exclusive control over sizing. But determining a workload’s capacity is a single, coupled mathematical equation: splitting it across decoupled controllers produces thrashing, not efficiency.
Four controllers, four narrow questions
Each of the four tools answers one precise question, and that is the crux of the problem. The Horizontal Pod Autoscaler (HPA) answers “how many replicas to hold the CPU load?” by comparing real usage to declared requests. The Vertical Pod Autoscaler (VPA) answers “what request size for a pod?” by observing historical usage and recommending new resources.requests. KEDA answers “how many replicas to drain the queue?” through event-driven scalers, from Kafka to Prometheus. Karpenter answers “which nodes to provision to host these pods?” by optimizing physical supply against declared demand.
The key is that these four questions concern variables that answer to one another. The replica count (HPA, KEDA) depends on pod size (VPA), which in turn depends on available nodes (Karpenter), which depend on the aggregated requests. The four controllers therefore manipulate the same system of equations without ever solving it together — and each one believes it is the sole owner.
The four collisions
When independent control loops manipulate interdependent variables, feedback loops destabilize into four recurring failure modes.
1. The HPA–VPA death spiral. HPA computes its target replicas from CPU utilization as a percentage of declared requests: metric = total usage ÷ declared requests. The denominator is the requests. When VPA lowers those requests to eliminate waste, it shrinks the denominator: a minor traffic increase now pushes the percentage past HPA’s threshold, triggering a horizontal scale-out. Pods split the traffic, per-pod usage drops, VPA shrinks requests further — and the workload enters a spiral of ever-smaller, ever-more-numerous containers.
2. The multi-writer race, HPA versus KEDA. When you combine KEDA’s queue-based scaling with standard CPU-based HPA, two controllers target the same deployment’s /scale sub-resource. An event spike makes KEDA scale up to drain the backlog, but if the worker pods are I/O-heavy with low CPU, HPA concludes the app is over-provisioned and scales back down. The API server records alternating rescale events while real throughput stalls.
3. The GitOps ping-pong. GitOps engines enforce the Git repository as the source of truth. When HPA scales a deployment during a spike, Argo CD flags it OutOfSync; with auto-sync enabled, it resets the deployment to its baseline, starving the app of capacity at the worst moment. Ignoring replica differences breaks declarative infrastructure: Git no longer reflects production, and future rollouts risk quietly restoring outdated baselines.
4. The multiplier effect at the node layer. Node autoscalers like Karpenter make decisions against declared requests, not actual consumption. A container requesting 1000m but using 150m causes an entire core to be reserved; Karpenter then provisions physical instances for phantom demand. Across production fleets, average cluster CPU utilization hovers around 8% despite heavy provisioning.
Automated is not autonomous
The post’s conclusion deserves to be read with its origin in mind: it is a CNCF-sponsored piece by Nikhil Kurup, VP of engineering at Sedai, a vendor of autonomous rightsizing. The pitch for a “unified control plane” therefore serves a commercial interest — but the diagnosis of the four collisions remains independently verifiable.
The useful distinction is lexical. Today’s autoscalers are automated: like biological reflexes, they measure one isolated signal and fire a predefined response. When five independent reflexes pull on the same limb, the system thrashes. Autonomy would require a closed loop that observes the entire environment, understands the performance–cost tradeoff, and takes end-to-end accountability for the outcome.
The final proposal has the virtue of restraint: HPA, VPA, KEDA, and Karpenter should keep executing the API mutations they were built for. What Kubernetes lacks is not a fifth actuator, but the central intelligence layer that decides what those actuators should do.
How to detect the collisions in your own cluster
The most visible symptom is a sequence of contradictory rescalings in the deployment history: kubectl describe hpa shows HPA scaling up and down with no traffic spike, while VPA recommendations keep lowering requests. Also check whether two controllers write to the same /scale sub-resource — that is the case as soon as KEDA and HPA target the same object. Finally, compare declared requests to actual usage: a large, persistent gap between resources.requests and measured consumption is the sign that Karpenter is provisioning for phantom demand, and that the cloud bill is absorbing cores that are never used.
Verdict
If you run HPA and VPA together on the same workload, stop: the death spiral described above is structural, not accidental. Pick one or the other depending on whether your bottleneck is CPU or request sizing.
If you combine KEDA and HPA on the same deployment, disable one or isolate them per workload: two writers on /scale produce alternating rescalings that kill throughput.
If you use GitOps with autoscaling, decide explicitly who owns the replicas field — the repository or the controller — and document the tradeoff, otherwise you restore dead baselines on every spike.
And to any vendor promising “autonomous sizing,” ask the one question that matters: who actually has the full picture of sizing, and has this system proven itself against your own SLO metric rather than a vendor benchmark.