
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
To successfully autoscale a .NET service on Kubernetes, you must move beyond default CPU thresholds and align scaling behavior with actual application demand. While the Horizontal Pod Autoscaler (HPA) works out of the box for basic resource metrics, production .NET workloads often require custom metrics from Prometheus or event-driven scaling via KEDA to handle bursty traffic without over-provisioning. This guide covers the exact configuration patterns I use to keep ASP.NET Core services responsive and cost-efficient.
How do you configure HPA to autoscale a .NET service on Kubernetes?
The foundation of any scaling strategy is getting the basics right. Before you even touch an HPA manifest, your .NET deployment must have explicit resource requests and limits defined. Without these, the Metrics Server has no baseline to calculate utilization percentages against, and your pods will be evicted unpredictably under load. I detail this prerequisite extensively in my guide on Kubernetes resource limits and requests, but for .NET specifically, remember that the garbage collector behaves differently when container-aware. Always set DOTNET_GCHeapHardLimitPercent or rely on .NET 8+ container defaults to prevent OOM kills during scale-up events.
Basic CPU/Memory HPA Manifest
For many internal APIs or background workers, standard resource metrics are sufficient. Here is a production-tested HPA configuration for an ASP.NET Core Web API:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: dotnet-api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: dotnet-api
minReplicas: 2
maxReplicas: 10
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 120
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75 Notice the behavior block. This is non-negotiable for .NET services. The default HPA settings can cause flapping—rapidly scaling up and down—which is devastating for JIT-compiled runtimes that need warm-up time. Setting a 300-second stabilization window for scale-down ensures your pods stay alive long enough to handle delayed requests or connection drains. For more on managing pod lifecycle issues during scaling, see debugging CrashLoopBackOff in Kubernetes.
When should you use custom metrics instead of CPU for .NET scaling?
CPU utilization is a lagging indicator for .NET applications. By the time CPU spikes above 70%, your thread pool may already be saturated, and incoming requests are queuing. Custom metrics allow you to scale proactively based on business-relevant signals like HTTP request rate, active connections, or queue depth. This approach aligns directly with the principles discussed in defining meaningful SLIs and SLOs.
Exposing .NET Metrics to Prometheus
First, instrument your ASP.NET Core application using OpenTelemetry or the Prometheus client library. You need to expose a metric that correlates with user-perceived latency. Request rate and concurrent requests are usually the best candidates:
// Program.cs - Minimal API Example
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddPrometheusExporter());
// Custom business metric for queue processing
private static readonly Counter<long> _queueDepth =
Meter.CreateCounter<long>("worker_queue_depth",
description: "Current items pending processing"); Once scraped by Prometheus, you need the Prometheus Adapter to translate these raw metrics into the Kubernetes Custom Metrics API format that HPA understands. Configure the adapter with a rule mapping your Prometheus query to an HPA-compatible metric name. This bridges the gap between observability and orchestration, a pattern central to the Prometheus and Grafana monitoring stack.
Custom Metric HPA Configuration
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "150" This tells Kubernetes to maintain approximately 150 requests per second per pod. If the aggregate rate hits 600 RPS across 3 pods, the HPA scales to 4. This is far more responsive than waiting for CPU saturation.
How does KEDA improve event-driven autoscaling for .NET?
If your .NET service processes messages from Azure Service Bus, RabbitMQ, Kafka, or AWS SQS, HPA alone is inadequate. Message queues can accumulate thousands of unprocessed items while CPU remains idle. KEDA (Kubernetes Event-Driven Autoscaling) solves this by scaling based on queue length or consumer lag rather than pod resource usage. It acts as an external metrics provider to the HPA, meaning you still get native Kubernetes scaling semantics with event-driven triggers.
Configuring KEDA ScaledObject for .NET Workers
KEDA’s ScaledObject CRD replaces the standard HPA manifest for event-driven workloads. Below is a configuration for a .NET worker consuming from Azure Service Bus:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: dotnet-worker-scaler
spec:
scaleTargetRef:
name: dotnet-worker
minReplicaCount: 0
maxReplicaCount: 8
pollingInterval: 15
cooldownPeriod: 120
triggers:
- type: azure-servicebus
metadata:
namespace: my-servicebus-namespace
queueName: orders-processing
messageCount: "100"
authenticationRef:
name: azure-sb-auth Key tuning parameters here: messageCount: "100" means KEDA targets 100 messages per replica. If the queue holds 500 messages, KEDA requests 5 replicas. Setting minReplicaCount: 0 enables true scale-to-zero, critical for cost optimization in dev/staging environments or low-traffic production services. The cooldownPeriod prevents premature scale-down after a burst, allowing your .NET worker to finish processing in-flight messages before terminating.
Authentication Best Practices
Never hardcode connection strings in KEDA triggers. Use TriggerAuthentication resources linked to Kubernetes Secrets or managed identity providers like Azure Workload Identity. This keeps credentials out of your GitOps repository and aligns with Kubernetes secrets management best practices. For Azure specifically, workload identity eliminates secret rotation entirely.
What are common pitfalls when autoscaling .NET on Kubernetes?
Even with perfect YAML, .NET runtime characteristics can undermine your scaling strategy. These are the failures I see most frequently in production audits:
- Ignoring JIT Warm-Up: Fresh .NET pods serve slow responses until the JIT compiler optimizes hot paths. If your HPA scales up during a traffic spike, new pods may timeout before warming up. Implement readiness probes that hit a dedicated
/health/readyendpoint only after warm-up completes, or use .NET 8’s Native AOT for near-instant startup. - Memory Limit Miscalculation: .NET’s GC reserves heap segments aggressively. If your memory limit is too tight relative to working set, the runtime triggers frequent Gen2 collections or gets OOM-killed. Profile your app under load and set limits at least 20–30% above observed peak usage.
- Missing Stabilization Windows: Without explicit
scaleDown.stabilizationWindowSeconds, HPA defaults to 5 minutes in newer versions but older clusters may use 0. Always declare it explicitly to avoid flapping. - Over-Reliance on CPU: Async .NET code can saturate I/O or thread pools while CPU stays below 50%. Combine CPU metrics with custom metrics like active DB connections or HTTP queue length for accurate scaling signals.
- Not Testing Scale Behavior: Load test your scaling configuration before production. Tools like k6 or Artillery can simulate traffic patterns to validate that scale-up latency meets your SLOs. See load testing with k6 for practical patterns.
| Scaling Approach | Best For | Complexity | Responsiveness | Cost Efficiency |
|---|---|---|---|---|
| HPA (CPU/Memory) | Stateless APIs, predictable load | Low | Moderate | Good |
| HPA + Prometheus Adapter | Latency-sensitive APIs, custom SLIs | Medium | High | Better |
| KEDA (Event-Driven) | Queue workers, event processors | Medium-High | Very High | Best (scale-to-zero) |
| VPA (Vertical) | Right-sizing single replicas | Low | N/A | Complementary |
Start Scaling Your .NET Services with Confidence
To reliably autoscale a .NET service on Kubernetes, start with properly configured resource-based HPA and explicit stabilization windows. Graduate to custom Prometheus metrics when CPU fails to capture real demand, and adopt KEDA for any event-driven or queue-based workload. Always validate your scaling behavior under realistic load before trusting it in production. If you need help designing a scaling strategy that balances performance, cost, and compliance for your .NET platform, reach out to discuss your architecture.