Scaling does not do what you expect
An HPA is not scaling
kubectl get hpa
kubectl describe hpa <name>The TARGETS column is the diagnosis. <unknown>/70% means the HPA cannot read a metric and will not act at all.
The container has no CPU request. This is the usual cause. averageUtilization is a percentage of the request, so with no request there is no baseline to take a percentage of.
Note that a container declaring no resources gets a namespace default, so the request exists but is small, and utilisation will read far higher than you expect. Either way the answer is to set the request deliberately, see Billing and quotas.
Metrics have not arrived yet. Right after a deployment there is nothing to average. Give it a minute before concluding anything.
It is scaling, just not as fast as you want. The HPA deliberately damps changes to avoid flapping. Watch it over a few minutes rather than seconds.
It is at maxReplicas. describe says so plainly, and the fix is a bigger maximum or a quota that allows one.
A sleeping workload never wakes, or never sleeps
Scale-to-zero problems announce themselves on the route rather than the workload, so look there first:
kubectl describe httproute <name>It never sleeps. Nearly always a second autoscaler is holding it awake. An HPA with minReplicas: 1 or higher keeps at least one replica running forever, so the workload never drops to zero and you keep paying for it. Nothing blocks you from adding one, but the route says so:
Warning ScaleToZeroHPAConflict HorizontalPodAutoscaler myapp also targets
Deployment/myapp ...Delete the HPA, or drop the scale-to-zero annotation if you would rather keep it. One autoscaler per workload.
If the opt-in was refused outright, the conflict already existed when you added the annotation:
Warning ScaleToZeroError the workload 'myapp' of type 'apps/v1.Deployment'
is already managed by the hpa 'myapp'It never sleeps and there is no other autoscaler. Something is still calling it. Health checks, uptime monitors and crawlers all count as traffic. The idle window is 300 seconds at minimum, so a monitor polling every minute keeps it awake permanently.
It sleeps but the first request fails. Waking normally takes a couple of seconds and the caller just waits. If the cluster has to add a node first, the wake takes longer than the connection is held and the caller gets an error page instead. It is rare, and a retry succeeds.
Waking is slower than it should be. Your readiness probe is the biggest single lever. No traffic is sent until it passes, so initialDelaySeconds: 5, periodSeconds: 10 means the earliest possible wake is five seconds and the next chance is fifteen. Use initialDelaySeconds: 0 and periodSeconds: 1 if wake latency matters.
It scales to more replicas than you expected, or not enough. The only signal is in-flight request concurrency. The platform default of 100 requests per replica is far too high for most applications, which is why the workload gets slower under load instead of scaling out. Set the target to roughly what one replica serves comfortably:
itsh.dev/scale-to-zero-target-concurrency: "10"If your load is not proportional to concurrent HTTP requests, scale-to-zero is the wrong tool. Use an HPA or a KEDA ScaledObject.
What's next
- Autoscaling for how each scaler works
- Scale-to-zero for the annotations in full