The Three Axes of Kubernetes Scaling
Summary
- Kubernetes scaling happens on three independent axes: pod count (horizontal), per-pod resources (vertical), and node capacity (cluster).
- Insight: These axes are not alternatives. A production system usually runs all three at once, and they only cooperate if their signals agree.
- Key Concepts: Horizontal Pod Autoscaler (HPA) changes replica count, Vertical Pod Autoscaler (VPA) changes requests/limits, and the Cluster Autoscaler or Karpenter changes node count.
- When to Use: Reach for horizontal scaling for stateless throughput, vertical for right-sizing, and cluster scaling to back both with real hardware.
- Limitations/Trade-offs: HPA and VPA conflict on CPU/memory targets, and node scaling adds minutes of latency that pod scaling cannot hide.
- Related Topics: Resource requests, the metrics pipeline, disruption budgets, and event-driven scaling with KEDA.
Foundations
Every scaling decision in Kubernetes answers one of three questions.
How many copies of this workload should run? That is the horizontal axis.
How much CPU and memory should each copy get? That is the vertical axis.
Is there enough node capacity to place those copies? That is the cluster axis.
The scheduler sits underneath all three. It places pods onto nodes based on their resource requests, not their actual usage.
This is the single most important fact in autoscaling. Requests drive scheduling and node math, while live usage drives horizontal scaling decisions.
Horizontal scaling is the default reflex for stateless services. More replicas mean more parallel request handlers behind a Service or Gateway.
Vertical scaling suits workloads that cannot be split easily. A single-threaded process, a JVM with a fixed heap, or a database sidecar often needs a bigger pod rather than more pods.
Cluster scaling is the foundation both others stand on. Pods cannot run on capacity that does not exist.
Mechanics & Interactions
The horizontal axis is driven by the HPA controller. It reads metrics, compares them to a target, and computes a desired replica count with a simple ratio.
The core formula is desiredReplicas = ceil(currentReplicas * (currentMetric / targetMetric)).
If your target is 60% CPU and pods sit at 120%, the HPA roughly doubles the replicas. Metrics come from metrics.k8s.io (the metrics-server) for CPU and memory, or from custom and external adapters for anything else.
The vertical axis is driven by the VPA. Its recommender watches historical usage and produces suggested requests, which its updater can apply by evicting and recreating pods.
The cluster axis reacts to the scheduler. When a pod is Pending because no node can satisfy its requests, the Cluster Autoscaler adds a node from a node group; when nodes sit underused, it drains and removes them.
Here is how a traffic spike flows through all three.
Traffic rises -> pod CPU climbs -> HPA raises replica count
-> new pods are Pending (no room) -> Cluster Autoscaler adds a node
-> scheduler places the pods -> load spreads and CPU settlesThe reverse path matters just as much. Traffic falls, the HPA scales replicas down, nodes empty out, and the Cluster Autoscaler consolidates them to cut cost.
Note the latency difference. Pod scaling completes in seconds once capacity exists, but node provisioning can take one to several minutes depending on the cloud.
Advanced Considerations & Applications
The classic mistake is running HPA and VPA against the same metric. If both react to CPU, the HPA adds replicas while the VPA grows each pod, and they oscillate.
The supported pattern is to let VPA manage memory (or run in recommendation-only mode) while HPA owns CPU or a custom throughput metric.
Node scaling introduces bin-packing decisions. Karpenter provisions right-sized nodes per pending workload, while the classic Cluster Autoscaler scales fixed-shape node groups.
Scale-down is where production systems get hurt. A PodDisruptionBudget protects availability by capping how many pods a voluntary eviction can remove at once.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: webEvent-driven scaling extends the horizontal axis beyond CPU. KEDA scales on queue depth, cron schedules, or Prometheus queries, including scale-to-zero, which the stock HPA cannot do on its own.
For batch and bursty work, the three axes still apply, but the trigger changes from CPU to backlog. The node axis remains the safety net underneath.
Common Misconceptions
"Autoscaling means the HPA." The HPA is only the horizontal axis. Without node scaling, the HPA stalls the moment the cluster runs out of room.
"HPA scales on actual usage versus capacity." It scales on usage versus your target, expressed as a percentage of the pod's request. No requests set means no CPU-based HPA.
"VPA and HPA work together automatically." They conflict on shared metrics. You must deliberately separate their responsibilities.
"Adding nodes fixes slow scaling." Nodes take minutes to join. For fast spikes you need headroom or over-provisioning pods, not just a reactive Cluster Autoscaler.
"Scale to zero is built in." The core HPA has a minimum of one replica in normal use. Scale-to-zero requires KEDA or a similar controller.
FAQs
Which axis should I configure first? Set correct resource requests, then add the HPA, then enable cluster scaling to back it. Requests are the prerequisite for both others.
Can I run all three at once? Yes, and most mature clusters do. The rule is that HPA and VPA must not fight over the same metric.
Why do my pods stay Pending during a spike? The HPA created replicas the cluster cannot place. That is the node axis signaling that the Cluster Autoscaler needs to add capacity.
Does horizontal scaling help a single-threaded app? Only if requests are distributed across replicas. If one request pins one pod, you may need the vertical axis instead.
How do I scale on something other than CPU? Use custom or external metrics through an adapter, or adopt KEDA for queue, cron, and Prometheus-driven triggers.
Is node scaling instant? No. Expect one to several minutes for a new node, which is why headroom matters for latency-sensitive spikes.
Related
- Autoscaling Basics
- Horizontal Pod Autoscaler
- Vertical Pod Autoscaler
- Cluster Autoscaler
- KEDA Event-Driven Scaling
- Autoscaling Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).