Running Kubernetes workloads continuously means paying for compute even when an application has little or no traffic. For services that receive requests only occasionally, keeping multiple Pods running can be inefficient.
Kubernetes 1.37 introduces an important improvement to the Horizontal Pod Autoscaler (HPA): scale-to-zero support graduates to Beta and is enabled by default. It allows workloads using object or external metrics to scale down to zero Pods when there is no work and automatically scale back up when demand returns.
This is particularly useful for queue consumers, batch-processing services, and workloads that use expensive resources such as GPUs.
For .NET developers, the feature provides an interesting way to reduce idle compute while introducing an important trade-off: when the application reaches zero replicas, the next request or piece of work has to wait for a new Pod to start.
What Is HPA Scale-to-Zero?
The Kubernetes Horizontal Pod Autoscaler normally adjusts the number of Pods according to observed metrics.
For example:
minReplicas: 1
maxReplicas: 10
means the workload will normally keep at least one Pod running.
With Kubernetes 1.37, an HPA using an object or external metric can instead use:
minReplicas: 0
When the metric indicates that no replicas are required, Kubernetes can scale the workload to zero.
When the metric later indicates that work is available, the HPA can increase the replica count again.
The important limitation is that CPU and memory resource metrics cannot be used to trigger scale-from-zero. Those metrics depend on running Pods, so Kubernetes requires an object or external metric for this behavior.
Why Scale-to-Zero Matters for .NET Applications
Consider a .NET worker that processes messages from a queue.
Without scale-to-zero:
Queue empty
↓
.NET Pod remains running
↓
CPU and memory continue to be allocated
↓
Compute cost continues
With scale-to-zero:
Queue empty
↓
HPA scales workload to 0
↓
No application Pods running
↓
New work arrives
↓
External metric increases
↓
HPA scales workload up
↓
.NET Pod starts processing
This can be useful when workloads spend significant periods idle.
Typical examples include:
Background queue consumers
Report-generation workers
Scheduled processing services
Image or document processing
Event-driven .NET workers
GPU-backed workloads
Low-frequency internal services
The Kubernetes project specifically identifies occasionally used queue consumers and GPU workloads as use cases for scale-to-zero.
Configuring a .NET Worker for Scale-to-Zero
Suppose a .NET worker processes messages from an external queue. The HPA could be configured using an external metric representing the amount of pending work.
A simplified HPA configuration looks like this:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: order-worker
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-worker
minReplicas: 0
maxReplicas: 10
metrics:
- type: External
external:
metric:
name: orders_pending
target:
type: Value
value: "10"
The exact external metric configuration depends on the metrics adapter and monitoring system used by the cluster.
The important parts are:
minReplicas: 0
and:
type: External
Kubernetes requires at least one object or external metric when minReplicas is zero.
A Simple .NET Worker Example
The application itself does not need special Kubernetes-specific code just to support scale-to-zero.
A typical .NET worker can continue processing messages normally:
public class OrderWorker : BackgroundService
{
private readonly ILogger<OrderWorker> _logger;
public OrderWorker(ILogger<OrderWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var message = await GetNextMessageAsync(stoppingToken);
if (message is null)
{
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
continue;
}
await ProcessOrderAsync(message, stoppingToken);
}
}
private Task<OrderMessage?> GetNextMessageAsync(
CancellationToken cancellationToken)
{
// Read from the configured message broker.
return Task.FromResult<OrderMessage?>(null);
}
private Task ProcessOrderAsync(
OrderMessage message,
CancellationToken cancellationToken)
{
// Process the order.
return Task.CompletedTask;
}
}
public record OrderMessage(int Id);
When the Deployment has zero replicas, this code is not running. Kubernetes starts the Pod again when the external metric causes the HPA to scale the workload up.
That makes startup behavior an important part of the architecture.
Cold Starts: The Main Trade-Off
Scale-to-zero reduces idle compute, but it does not make the workload instantly available.
Suppose the following happens:
No workload
↓
0 Pods
↓
Message arrives
↓
Metric changes
↓
HPA calculates desired replicas
↓
Deployment creates Pod
↓
Container image starts
↓
.NET runtime starts
↓
Application initializes
↓
Readiness probe succeeds
↓
Work can be processed
Every step introduces some latency.
For a .NET application, startup can include:
Container image download
.NET runtime initialization
Dependency injection setup
Configuration loading
Database connection establishment
Cache initialization
External service initialization
Application readiness checks
Therefore, scale-to-zero is not automatically appropriate for latency-sensitive workloads.
Cost vs Cold Start
The trade-off can be summarized as follows:
Join the conversation! Your thoughts help the community grow.