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.
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 Sizein 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.