Scaling out, not up
When a service gets busy, you have two options: make each instance bigger (vertical scaling) or run more instances (horizontal scaling). Horizontal scaling is the cloud-native default — it's more resilient (no single big point of failure), can scale further, and works without restarting workloads. In Kubernetes, the Horizontal Pod Autoscaler (HPA) automates it: it watches a metric and adjusts the number of pod replicas to match demand.
How the control loop works
The HPA is a controller that runs a loop, by default about every 15 seconds:
- 1Observe the current value of the target metric across the pods (e.g. average CPU utilization).
- 2Compute the desired replica count to bring the metric to its target.
- 3Act by scaling the Deployment/ReplicaSet up or down.
The core formula is simple:
desiredReplicas = ceil(currentReplicas * (currentMetricValue / desiredMetricValue))Example: 4 replicas averaging 80% CPU, target 50%. ceil(4 * (80/50)) = ceil(6.4) = 7 replicas. The HPA scales to 7.
Resource requests are not optional
This trips people up constantly: CPU/memory-based HPA scales on utilization relative to the pod's resource *request*, not absolute usage. If your pods have no CPU request set, percentage-based autoscaling has no denominator and won't work correctly.
resources:
requests:
cpu: 250m # HPA target % is measured against THIS
memory: 256MiSet sensible requests, or the HPA's math is meaningless.
A basic HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60Keep average CPU near 60%; never below 2 replicas, never above 20.
Beyond CPU: custom and external metrics
The autoscaling/v2 API lets you scale on more than CPU/memory:
| Metric type | Source | Example |
|---|---|---|
Resource | Built-in | CPU, memory utilization |
Pods | Custom metrics adapter | requests/sec per pod |
Object | Custom metrics adapter | ingress request rate |
External | External metrics adapter | queue depth, lag |
For Pods/Object you need a metrics adapter (e.g. Prometheus Adapter) exposing the custom metric. For external event sources (queues, Kafka lag) most teams use KEDA, which feeds external metrics into an HPA *and* adds the one thing HPA can't do natively — scale to zero.
Avoiding thrash: stabilization and behavior
Naive autoscaling flaps — scaling up and down repeatedly around a threshold, churning pods. Modern HPA has a behavior section to tame it:
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # wait 5 min before scaling down
policies:
- type: Percent
value: 50 # remove at most 50% of pods per step
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0 # scale up promptly
policies:
- type: Percent
value: 100 # can double quickly under load
periodSeconds: 30The usual pattern: scale up fast, scale down slow. You'd rather over-provision briefly than drop capacity during a temporary dip.
What the HPA does *not* do
Knowing the boundaries prevents misuse:
- It doesn't change pod size. That's the Vertical Pod Autoscaler (VPA) — and you generally shouldn't run VPA and HPA on the same CPU/memory metric (they fight).
- It doesn't add nodes. If there's no room to schedule new pods, they sit
Pending. You need the Cluster Autoscaler (or Karpenter) to add nodes. - It doesn't scale to zero. Minimum is 1 (use KEDA for zero).
The full autoscaling story is HPA (replicas) + Cluster Autoscaler (nodes), and KEDA when you need event-driven or scale-to-zero behavior.
How a platform applies this
You shouldn't have to author HPA YAML and tune behavior windows for every app. On PandaStack, apps deploy via Helm to multi-region GKE with autoscaling configured as part of the release — the HPA scales your app's replicas against its compute tier's resource requests. Compute tiers range from Free (0.25 CPU/512MB) up to C2-2XCompute (8 CPU/16GB), so the request values the HPA measures against come from the tier you pick. For free-tier apps that need to go all the way to zero when idle, the platform layers KEDA scale-to-zero on top — the capability plain HPA lacks.
The takeaway: HPA handles the 1→N elasticity, KEDA handles the 0↔1 idle case, and the platform wires both so you just choose a tier.
Practical tips
- Always set resource requests — HPA depends on them.
- Pick a realistic target (e.g. 60–70% CPU) to leave headroom for spikes during scale-up lag.
- Tune scale-down stabilization to avoid thrash.
- Pair with Cluster Autoscaler so pods aren't left
Pending. - Don't HPA and VPA the same metric.
- Load test to confirm the metric you scale on actually correlates with user-facing load.
References
- [Kubernetes HPA documentation](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/)
- [HPA walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/)
- [Kubernetes resource requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
- [Cluster Autoscaler FAQ](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md)
- [KEDA documentation](https://keda.sh/docs/)
---
Want autoscaling configured for you — HPA elasticity plus scale-to-zero when idle? PandaStack wires both into every deploy. Pick a compute tier and push code at [dashboard.pandastack.io](https://dashboard.pandastack.io).