.NET Runtime Configuration Knobs You Should Know
The .NET runtime exposes dozens of configuration settings that control garbage collection, threading, JIT compilation, globalisation, and diagnostics. Most applications never need to touch them — the defaults are well-tuned. But when you're running in constrained environments, debugging production issues, or squeezing out performance, knowing which knobs to turn is invaluable.
Where to set them
Runtime configuration can be set in three places, in order of precedence:
1. Environment variables
DOTNET_GCServer=1
DOTNET_EnableDiagnostics=0
2. runtimeconfig.json
This file sits alongside your published binary. The template is generated from your project's runtimeconfig.template.json:
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true,
"System.GC.HeapHardLimit": 268435456
}
}
}
Set properties in your .csproj to populate the template:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption
Include="System.GC.HeapHardLimit"
Value="268435456" />
</ItemGroup>
3. MSBuild properties
Some settings have dedicated MSBuild properties (like ServerGarbageCollection above) that write to the runtimeconfig automatically.
Garbage collection
Server vs workstation GC
{
"configProperties": {
"System.GC.Server": true
}
}
Server GC allocates a heap and a dedicated GC thread per logical processor. It's optimised for throughput in multi-threaded server applications. Workstation GC uses a single heap and GC thread, optimised for responsiveness in client applications.
ASP.NET Core enables Server GC by default. Console applications and worker services default to Workstation GC — consider switching to Server GC for high-throughput background processors.
Heap hard limit
In containerised environments, set a memory ceiling:
{
"configProperties": {
"System.GC.HeapHardLimit": 268435456
}
}
The value is in bytes (256 MB in this example). When the GC hits this limit, it performs more aggressive collections rather than requesting more memory from the OS. This prevents container OOM kills.
Alternatively, use a percentage of the container's memory limit:
{
"configProperties": {
"System.GC.HeapHardLimitPercent": 75
}
}
GC regions (default from .NET 8)
From .NET 8, the GC uses a regions-based memory model by default instead of segments. Regions are smaller, more flexible memory units that reduce fragmentation. You can disable it if you encounter issues:
{
"configProperties": {
"System.GC.Regions": false
}
}
But there's rarely a reason to — regions are a strict improvement for most workloads.
Threading
Thread pool configuration
The thread pool's minimum thread count can be set to avoid slow ramp-up under burst load:
ThreadPool.SetMinThreads(workerThreads: 100, completionPortThreads: 100);
Or via configuration:
{
"configProperties": {
"System.Threading.ThreadPool.MinThreads": 100
}
}
// WARNING
Setting this too high wastes memory and can actually reduce performance. Only increase it if you've confirmed thread pool starvation via diagnostics.
Thread pool upper limit
{
"configProperties": {
"System.Threading.ThreadPool.MaxThreads": 500
}
}
Again, rarely needed — the thread pool's hill-climbing algorithm is generally better at finding the right thread count than a static configuration.
Diagnostics
Disabling diagnostics entirely
For high-security or ultra-lean deployments:
DOTNET_EnableDiagnostics=0
This disables the diagnostic port, EventPipe, and debugger attach. Useful in production containers where you don't want external tools connecting to the process.
Event pipe providers
Enable specific diagnostic providers at startup:
DOTNET_DiagnosticPorts=*:connect
Or configure event sources in runtimeconfig.json for always-on diagnostics.
Globalisation
Invariant mode
If your application doesn't need culture-specific formatting:
{
"configProperties": {
"System.Globalization.Invariant": true
}
}
This eliminates the dependency on ICU libraries, which is particularly useful for Alpine Linux containers and Native AOT deployments where the ICU data adds significant size.
<!-- Or via MSBuild -->
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
Caution: Invariant mode changes sorting, string comparison, and date/number formatting behaviour. Test thoroughly if your application handles user-facing text in multiple locales.
Predefined cultures only
A middle ground — load only the cultures you actually need:
{
"configProperties": {
"System.Globalization.PredefinedCulturesOnly": true
}
}
HTTP and networking
HTTP/3 support
Enable HTTP/3 (QUIC) for HttpClient:
{
"configProperties": {
"System.Net.SocketsHttpHandler.Http3Support": true
}
}
Connection pool limits
DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_MAXCONNECTIONSPERSERVER=100
This caps the number of concurrent connections HttpClient opens to a single server.
Practical advice
- Start with defaults. The .NET team benchmarks extensively. Override only when you have data showing a problem.
- Use environment variables in containers. They're easy to change without rebuilding.
- Use runtimeconfig for deployable defaults. Bake sensible settings into your published output.
- Monitor before and after. Use
dotnet-countersor Application Insights to verify that a configuration change actually helps.
Wrapping up
Runtime configuration knobs are power-user tools. The GC settings (server mode, heap limits) are the ones you'll reach for most often, followed by globalisation invariant mode for container deployments. For everything else, the defaults are excellent — but it's good to know the levers exist when you need them.