Autoscale a .NET Service on Kubernetes

Khimananda Oli 7 min read Programming and Languages
Autoscale a .NET Service on Kubernetes

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.

.NET ServiceASP.NET CoreMetrics ServerCPU / MemoryPrometheus AdapterCustom MetricsKEDAEvent SourcesHPA ControllerScale DecisionScale Pods
Three data paths to autoscale a .NET service on Kubernetes: resource metrics, custom Prometheus metrics, and external event triggers.

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.

.NET PodPrometheusAdapterHPA1. Scrape /metrics2. Query Range3. Custom Metric API4. Scale ReplicaSet
The four-step feedback loop to autoscale a .NET service on Kubernetes using Prometheus custom metrics.

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/ready endpoint 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 ApproachBest ForComplexityResponsivenessCost Efficiency
HPA (CPU/Memory)Stateless APIs, predictable loadLowModerateGood
HPA + Prometheus AdapterLatency-sensitive APIs, custom SLIsMediumHighBetter
KEDA (Event-Driven)Queue workers, event processorsMedium-HighVery HighBest (scale-to-zero)
VPA (Vertical)Right-sizing single replicasLowN/AComplementary
Responsiveness →Complexity →HPA BasicCPU/Mem OnlyCustom MetricsPrometheus AdapterKEDAEvent-Driven+Adapter+Triggers
Trade-off visualization: choosing the right method to autoscale a .NET service on Kubernetes based on complexity versus responsiveness.

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.

Frequently Asked Questions

Deploy the Metrics Server and define a HorizontalPodAutoscaler resource targeting your .NET Deployment. Specify CPU or custom metrics thresholds in the spec to trigger scaling events automatically based on observed load.

Yes, set averageMemoryUtilization in the HPA spec. Memory-based scaling suits .NET apps with large heaps or cache-heavy workloads where CPU remains low but RAM consumption indicates true load pressure.

KEDA extends HPA with event-driven triggers like queue depth or HTTP request rate. It scales .NET services from zero based on external signals, unlike standard HPA which relies solely on pod resource metrics.

Set minReplicas above zero and configure readiness probes accurately. Use .NET 9 native AOT compilation to reduce startup time significantly, ensuring new pods serve traffic quickly during scale-out events.

VPA adjusts CPU and memory requests per pod but conflicts with HPA if both manage replicas. Use VPA only in recommendation mode for .NET right-sizing, then apply values manually or via GitOps.

Use Kind or Minikube with Metrics Server installed. Simulate load using k6 or Locust against your .NET service endpoint and verify HPA status with kubectl get hpa to confirm replica adjustments occur correctly.

Prioritize request latency percentiles or active connection counts over raw CPU. These reflect actual user experience degradation in ASP.NET Core apps better than resource utilization alone, preventing under-scaling during high-throughput scenarios.

Cluster Autoscaler adds nodes when HPA cannot schedule new .NET pods due to insufficient resources. Ensure node pools have appropriate taints and tolerations so .NET workloads land on correctly sized instances.

No, standard HPA requires at least one replica. Use KEDA with an external scaler or ScaledObject to achieve true scale-to-zero for idle .NET services, reducing costs during off-peak hours.

Configure stabilizationWindowSeconds in the HPA behavior spec. Set longer cooldown periods for scale-down actions to prevent rapid oscillation when .NET garbage collection or JIT warmup causes temporary metric spikes.

HPA calculates utilization as current usage divided by requested resources. Underestimating .NET memory or CPU requests inflates utilization percentages, causing premature scaling; always base requests on production profiling data.

Yes, Prometheus Adapter exposes custom metrics from Prometheus as Kubernetes API resources. Configure adapter rules mapping .NET application metrics like request duration histograms to HPA-compatible metric names for event-aware scaling decisions.

Apply RBAC policies restricting HPA creation to specific namespaces. Use OPA Gatekeeper to enforce minimum maxReplicas limits and prevent runaway .NET deployments from consuming excessive cluster resources across tenants.

Yes, Gen2 collections pause all threads, causing temporary metric gaps that delay HPA reactions. Tune GC settings or switch to DATAS mode in .NET 9 to smooth throughput and improve scaling signal reliability.

Aggressive thresholds increase pod churn and node provisioning frequency, raising cloud spend. Balance responsiveness with stabilization windows and right-sized requests to optimize cost while maintaining acceptable latency SLAs for .NET services.