FR
live

Kubernetes 1.37 scales queue consumers to zero replicas with the HPA

On September 2, 2026, Kubernetes 1.37 enabled horizontal scaling to zero replicas by default (Beta) whenever an object or external metric, such as a queue length, allows it. Queue consumers and batch processors can release reserved CPU and GPU while idle, provided they accept the cold-start latency.

A row of identical grey server rack units, one empty slot edged with a single amber light.

Kubernetes 1.16. HorizontalPodAutoscaler scaling to zero enters Alpha. September 2, 2026. The feature graduates to Beta and turns on by default in Kubernetes 1.37, with the ScaledToZero condition to tell an automatic scale-down from a manual pause. 2026. The effort, owned by SIG Autoscaling, took seven years to mature. Why it matters: for the first time, dropping a Deployment to zero replicas when its queue is empty is part of core Kubernetes, with no external add-on.

Why scaling to zero changed the metric

The HPA typically scales on CPU or memory. The trap is mechanical: both metrics come from running Pods. Once the replica count reaches zero, there is no Pod left to measure, and no signal to tell the HPA to scale back up. Reaching zero therefore required an add-on like KEDA, an external component, or enabling an Alpha feature gate.

Object and external metrics do not have that limitation. A queue length exists independently of the workers that consume it: the HPA can keep reading it while no worker runs. That change of metric is what makes native scale-to-zero possible. The trade-off is cold start: the HPA must observe the metric, schedule a Pod and start the application before the work is processed.

An HPA driven by the queue

The canonical example is a queue consumer whose lag is tracked by Prometheus. The queue_consumer_lag series is exposed to the HPA through a metrics adapter — the reference implementation is the Prometheus Adapter, which publishes the series through the external metrics API:

yaml
externalRules:
- seriesQuery: '{__name__="queue_consumer_lag",name!=""}'
  metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>}) by (name)
  resources:
    overrides:
      namespace:
        resource: namespace

Before creating the HPA, verify that Kubernetes can read the metric — otherwise the HPA can never climb back from zero:

bash
kubectl get --raw \
  '/apis/external.metrics.k8s.io/v1beta1/namespaces/default/queue_consumer_lag?labelSelector=name%3Dworker_tasks'

The HPA below targets a queue-worker Deployment, allows between zero and ten replicas and requests one replica per 30 queued tasks:

yaml
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_consumer_lag
        selector:
          matchLabels:
            name: worker_tasks
      target:
        type: Value
        value: "30"

With an empty queue, the HPA reduces the Deployment to zero replicas; when tasks arrive, the external metric stays available and the HPA recomputes a replica count capped by maxReplicas. The default five-minute downscale stabilization window stops a brief drop in queue length from removing all workers at once: you can tune it through spec.behavior.scaleDown.

The ScaledToZero condition, or how to tell pause from stop

Setting a Deployment to zero creates an ambiguity: zero replicas can mean the HPA scaled it down, or that an operator paused it manually. Historically, setting a Deployment to zero by hand froze autoscaling.

The controller resolves this with a ScaledToZero status condition. When the HPA moves a workload from one or more replicas to zero, it records ScaledToZero=True: later reconciliation knows the controller owns that zero state and keeps evaluating the metrics. After scaling back up, the condition flips to ScaledToZero=False with the reason NotScaledToZero. A workload at zero without ScaledToZero=True stays considered paused — the HPA will not wake it, as before.

bash
kubectl describe hpa queue-worker

If the adapter cannot return the metric, the HPA reports ScalingActive=False with a reason like FailedGetExternalMetric. Restore the metric or scale the workload up manually to recover capacity. This is the main watchpoint: an HPA at zero depends entirely on its metric being available.

What not to do

The feature has sharp limits, and ignoring them is costly. Kubernetes Services do not buffer requests while no Pod is ready: an HTTP or request-driven workload scaled to zero will fail requests rather than wait. Those cases need a separate buffering layer — precisely the niche of KEDA, which scales on queue depth or a Kafka topic and knows how to expose a scaled object.

Two operational constraints complete the picture. minReplicas: 0 requires at least one object or external metric: the API server rejects an HPA that only contains resource metrics such as CPU or memory. And during a version-skewed upgrade, wait until both kube-apiserver and kube-controller-manager support the feature before creating zero-scaled HPAs: a controller with the feature gate disabled treats replicas: 0 as a manual pause and may leave a workload stuck at zero.

The good news is that the failure modes are explicit. ScaledToZero, ScalingActive and FailedGetExternalMetric are observable states, not silent guesses: a team that watches its HPA conditions will see a broken metric pipeline before it becomes a silent outage.

Native HPA or KEDA: the real trade-off

The question is not “which is better” but “which semantics your workload expects”. The native zero-scaled HPA fits the simple case: a queue consumer whose work can wait in a durable queue, and whose Pods reserve expensive resources — dedicated CPU, GPU. The savings peak there: a GPU worker pool running idle burns money continuously.

KEDA remains ahead whenever the workload is event-driven in the broad sense: Kafka, RabbitMQ, triggered functions, or any source that needs a specialized scaler and fine-grained buffering. The HPA’s progress does not replace KEDA; it shrinks the need for it in the most common case.

The math that makes scale-to-zero pay

The saving is measured in reserved resources, not replicas. A pool of ten GPU workers idling at zero load reserves ten GPUs around the clock — a cost that keeps running even when no task is queued. Scaling to zero does not remove the cost of the underlying infrastructure, but it frees the nodes for other workloads: in a cluster where GPUs are scarce, a stopped worker is a GPU handed back for training or a batch.

The gain only holds if the work can wait. A durable queue — Redis, RabbitMQ, Kafka or a managed cloud queue — is the entry condition: if the work must be processed immediately, the cold-start latency becomes a product defect, not an infrastructure compromise.

The concrete tipping point sits on latency. The HPA observes the metric on a regular interval: between a task arriving and a replica scaling back up, tens of seconds can pass, more with the cold start. KEDA, by subscribing directly to the event source, shrinks that delay. For a nightly batch or a consumer that tolerates thirty seconds of lag, the difference is negligible; for a low-latency queue, it is not.

Finally, keep the hidden coupling in mind: a zero-scaled HPA depends entirely on its metric being available. If Prometheus or the adapter goes down, the HPA cannot climb back up — a monitoring outage becomes a worker-pool outage. That is the price to fold into the saving calculation, especially since scaling to zero targets exactly the quiet periods.

Verdict

If you run a queue consumer whose Pods reserve expensive resources and whose work can wait in a durable queue, move to Kubernetes 1.37 and declare minReplicas: 0 with an external metric: you cut idle-period cost with no add-on.

If your workload is HTTP or request-driven, do not scale to zero with the HPA alone: keep a buffering layer, or stay on KEDA to absorb requests during startup.

In every case, exercise the climb back from zero in real conditions before enabling it in production, and watch ScalingActive: an HPA that can no longer read its metric is a workload that will not restart.

References

The cyber brief, every Tuesday

The flaws that matter and the patches to apply, in a ten-minute read.

No spam. One-click unsubscribe.
read next

On the same topic

GitHub Actions adds a vulnerability-alerts token and reusable workflow identity

On September 3, 2026, GitHub shipped three GitHub Actions updates: a vulnerability-alerts permission for GITHUB_TOKEN, the job context for reusable workflows, and a runner deprecation API. Swap your broad scopes for the vulnerability-alerts permission and adopt job.workflow_ref in your reusable workflows.

GitHub CLI’s signing key expires September 5, breaking Linux package installs

On Saturday, September 5, 2026, the PGP key that signs GitHub CLI’s APT and RPM repositories expires, and any gh install done before April 8 without a keyring update will start failing. Check your local keyring before the deadline and add the replacement key 7F38BBB59D064DBCB3D84D725612B36462313325.

← Back to the feed

Type at least two characters.

navigate open esc dismiss