Backend 8 min read

Performance Tuning .NET Core Microservices on Kubernetes

Garbage collection tuning, memory limits, connection pooling, and container lifecycle optimizations for .NET Core services in high-concurrency environments.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira

Running .NET Core microservices within containerized Kubernetes clusters provides immense flexibility, but standard default configurations often lead to unexpected memory spikes and container throttling under peak load.

In this guide, we dive into key performance optimizations for running .NET Core workloads smoothly on Kubernetes Container-as-a-Service (CaaS) platforms.

1. Configuring the .NET Garbage Collector for Containers

By default, .NET uses Server Garbage Collection (Server GC), which allocates one heap per CPU core. In constrained container environments with strict memory limits, Server GC can cause out-of-memory (OOM) kills if not configured correctly.

In your *.csproj or container runtime environment, configure GC container awareness:

<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
  <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
  <RetainVM>false</RetainVM>
</PropertyGroup>

Or via Dockerfile environment variables:

ENV DOTNET_gcServer=1 \
    DOTNET_GCDynamicAdaptationMode=1 \
    DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1

2. Managing HttpClient & Database Connection Pools

One common pitfall in high-throughput services is socket exhaustion caused by naive HttpClient instantiation. Always leverage IHttpClientFactory to pool underlying HTTP message handlers:

builder.Services.AddHttpClient("GrantApiClient", client =>
{
    client.BaseAddress = new Uri(builder.Configuration["Services:GrantApi"]);
    client.Timeout = TimeSpan.FromSeconds(10);
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5)); // Prevents stale DNS issues

[!TIP] For SQL Server connections, configure Max Pool Size in the connection string to align with the maximum expected concurrent queries per container pod, preventing pool saturation.

3. Kubernetes Pod Resource Limits & Probes

Avoid setting equal CPU requests and limits. Allow bursts while constraining memory strictly to prevent node starvation:

resources:
  requests:
    cpu: "250m"
    memory: "512Mi"
  limits:
    cpu: "1000m"
    memory: "1Gi"
livenessProbe:
  httpGet:
    path: /healthz/live
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /healthz/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

Wrap Up

Tuning container GC, reusing sockets via IHttpClientFactory, and specifying precise Kubernetes resource limits guarantees that your .NET Core services remain snappy and resilient even during intense traffic surges.

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.