A HorizontalPodAutoscaler can reduce the replica count of a Deployment, but minReplicas: 0 changes the question from how to scale down to how a workload starts again. A CPU or memory metric describes Pods that already exist. When the target has zero Pods, there is no Pod CPU or memory value for the controller to use as a wake-up signal.
Kubernetes v1.37 moves HPA scale-to-zero support to beta and enables it by default. The supported use is an HPA using an external or object metric, such as queue depth, a pending-work count, or another signal that remains available while the workload has no Pods. The release notes explicitly exclude CPU and memory metrics from scale-to-zero because they depend on active Pods.
This guide adds a small offline gate for autoscaling/v2 manifests. It rejects an HPA that combines minReplicas: 0 with only resource metrics, while accepting an HPA with an external or object metric. The gate is deliberately narrower than a general Kubernetes validator. It does not prove that a metric adapter works, that the target can start, or that the workload will process traffic. It makes one deployment assumption visible before a manifest reaches a cluster.
The validation run used the official kubectl v1.37.0 Linux AMD64 binary and Python 3.11.15. The downloaded kubectl checksum matched the Kubernetes-provided SHA-256 file. The gate accepted an external-metric HPA with exit status zero and rejected a CPU-only HPA with exit status one. In the same isolated environment, kubectl apply --dry-run=client --validate=strict could not validate either manifest without an API server because it attempted to download OpenAPI data. That is why this offline check complements, rather than replaces, server-side dry-run validation in a staging cluster.
Start with a metric that can exist at zero replicas
For a queue consumer, the usual wake-up signal is a count outside the consumer Pods. An external metrics adapter can expose that count to the HPA controller. The HPA then compares the current value with the target and adjusts the Deployment replica count. When the queue is empty, the controller can hold the Deployment at zero; when work appears, the external metric remains queryable and can drive the first replica back up.
Use a metric with clear ownership and a documented failure mode. A queue-depth signal may be stale, delayed, or unavailable. Decide ahead of time whether a missing value should prevent scale-down, trigger an alert, or use a conservative minimum. Do not treat an HPA status of AbleToScale: True as proof that an external metric is current. Inspect the metric adapter and the source system during staging.
This is a minimal HPA for a Deployment named queue-worker:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: queue-worker
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: queue-worker
minReplicas: 0
maxReplicas: 10
metrics:
- type: External
external:
metric:
name: queue_messages_ready
target:
type: Value
value: "1"
The metric name is an example, not a Kubernetes built-in. It must match the name served by the adapter installed in the target cluster. type: Value compares the external metric as a whole with the target value. A queue worker may instead need AverageValue when a per-Pod target describes the intended work distribution. Choose that threshold from staging observations, not from a generic example.
Keep maxReplicas finite and set a value appropriate for the service, its downstream capacity, and the available cluster resources. HPA can create more replicas in response to a high metric, but it cannot add node capacity by itself. If a workload needs node autoscaling, test that path as a separate system: a queue spike, the first Pod scheduling, a node becoming available, and the worker consuming the queued item are distinct events.
Make the unsupported combination visible in review
The following script reads JSON-form HPA manifests and applies one narrow rule: when minReplicas is zero, at least one metric must be External or Object. JSON is valid YAML, so repositories that keep YAML can either render the HPA to JSON before this check or implement the same rule in their existing policy tool.
Save the script as scripts/validate-hpa-scale-to-zero.py and run it with the Python version already used by the build environment:
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
def metric_type(metric):
return metric.get("type") if isinstance(metric, dict) else None
def main(paths):
failures = []
for name in paths:
document = json.loads(Path(name).read_text(encoding="utf-8"))
if document.get("apiVersion") != "autoscaling/v2" or document.get("kind") != "HorizontalPodAutoscaler":
continue
spec = document.get("spec", {})
if spec.get("minReplicas") != 0:
continue
types = {metric_type(metric) for metric in spec.get("metrics", [])}
if not ({"External", "Object"} & types):
failures.append(
f"{name}: minReplicas: 0 requires an External or Object metric"
)
if failures:
print("\n".join(failures), file=sys.stderr)
return 1
print("PASS: scale-to-zero HPAs use an External or Object metric")
return 0
raise SystemExit(main(sys.argv[1:]))
The script intentionally does not accept a Resource metric merely because another metric might be added later by a chart value. It checks the rendered manifest passed to it. Run it after templating and before a deployment step so the object reviewed by the gate is the object the cluster will receive.
It also leaves HPAs with minReplicas greater than zero alone. CPU and memory utilization are useful metrics for a running service. The rule only guards the point where resource utilization cannot create the first Pod. If a workload uses several HPAs or an external controller owns replicas, establish ownership before adding this rule; conflicting writers to .spec.replicas create a different operational problem.
Test the positive and negative cases
Use two non-production fixtures. The first is the external-metric HPA above, saved as queue-worker-hpa.json. The second has the same target and replica bounds but uses a CPU resource metric:
{
"apiVersion": "autoscaling/v2",
"kind": "HorizontalPodAutoscaler",
"metadata": {"name": "cpu-only-worker"},
"spec": {
"scaleTargetRef": {
"apiVersion": "apps/v1",
"kind": "Deployment",
"name": "cpu-only-worker"
},
"minReplicas": 0,
"maxReplicas": 10,
"metrics": [{
"type": "Resource",
"resource": {
"name": "cpu",
"target": {"type": "Utilization", "averageUtilization": 70}
}
}]
}
}
Run both sides of the policy:
python3 scripts/validate-hpa-scale-to-zero.py queue-worker-hpa.json
set +e
python3 scripts/validate-hpa-scale-to-zero.py cpu-only-hpa.json
status=$?
set -e
test "$status" -eq 1
The tested positive case printed PASS: scale-to-zero HPAs use an External or Object metric and exited zero. The negative fixture printed a diagnostic naming cpu-only-hpa.json and exited one. Keeping both fixtures matters: a gate that only has an accepted example can silently stop recognizing the unsafe combination after a later edit.
Do not substitute a local kubectl dry run for this semantic test. In the validation environment, strict client-side validation tried to retrieve OpenAPI data from an API server and failed because no cluster was configured. A client configured for a real cluster can validate API shape, but it cannot establish that the metric adapter exposes the required metric or that the workload wakes from zero. Those checks belong in a disposable namespace or staging cluster.
Validate the full path in staging
Before enabling minReplicas: 0 in production, apply the reviewed manifest with server-side dry-run in an authorized staging context. Confirm that the target Deployment is scalable, the external metrics API is registered, and the adapter returns the exact metric name and labels used by the HPA. Then create one harmless unit of work and observe the HPA status, Deployment replica count, Pod readiness, and successful processing.
Repeat the test after the queue becomes empty. Confirm that replicas return to zero only after the chosen stabilization behavior, and that a second work item starts the target again. Capture only redacted status output in the change record; metric labels, namespace names, service names, and endpoint addresses can reveal internal topology.
A scale-to-zero HPA is useful when its wake-up signal exists independently of the Pods it controls. The small manifest gate catches the easy-to-review mismatch early. A staging exercise then proves the part a static file cannot: that the metric path, scheduler, application startup, and work source operate together.