The .NET garbage collector has two fundamental modes: workstation and server. Choosing the wrong one can leave performance on the table or waste memory. Understanding the difference is essential for any production .NET application.
Workstation GC
Workstation GC is the default for most .NET applications. It uses a single GC heap and performs collections on the thread that triggered the allocation. Its characteristics:
- Single heap — all allocations go to one managed heap.
- Lower memory usage — only one set of GC data structures.
- Lower throughput — collections pause the application on a single thread.
- Better for client apps — designed for desktop applications where responsiveness matters more than throughput.
Workstation GC is the default when an application runs on a machine with fewer than 2 cores, or when explicitly configured.
Server GC
Server GC creates one heap per logical processor. Each heap has its own GC thread, and collections run in parallel across all heaps. Its characteristics:
- Multiple heaps — one per logical processor, reducing contention.
- Higher memory usage — each heap maintains its own segments and free lists.
- Higher throughput — parallel collection across all cores.
- Better for server apps — designed for web servers and services handling many concurrent requests.
On an 8-core machine, server GC creates 8 heaps and 8 dedicated GC threads. This means collections are ~8x faster in wall-clock time, but use ~8x more memory for GC bookkeeping.
Configuration
Set the GC mode in your project file:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Or in runtimeconfig.json:
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
}
ASP.NET Core projects use server GC by default. Console applications use workstation GC by default.
Concurrent vs Background GC
Both workstation and server modes support background GC (enabled by default). Background GC performs Gen 2 collections on a separate thread, allowing application threads to continue running during most of the collection.
<PropertyGroup>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
With background GC disabled, Gen 2 collections are blocking — all application threads are suspended for the entire duration. This is rarely desirable, but can be useful in batch processing scenarios where consistent throughput matters more than latency.
Measuring GC Impact
Use dotnet-counters to observe GC behaviour in real time:
dotnet-counters monitor -p <pid> System.Runtime --counters \
gc-heap-size,gen-0-gc-count,gen-1-gc-count,gen-2-gc-count,\
time-in-gc,alloc-rate
Key metrics to watch:
- % Time in GC — above 10% suggests GC is a bottleneck.
- Gen 2 collections — should be rare. Frequent Gen 2 collections indicate too many long-lived allocations.
- Allocation rate — higher rates drive more frequent collections.
You can also use GC.GetGCMemoryInfo() programmatically:
var info = GC.GetGCMemoryInfo();
Console.WriteLine($"Heap size: {info.HeapSizeBytes / 1024 / 1024} MB");
Console.WriteLine($"Compacted: {info.Compacted}");
Console.WriteLine($"Concurrent: {info.Concurrent}");
Console.WriteLine($"Generation: {info.Generation}");
DATAS: Dynamic Adaptation to Application Sizes
.NET 7 introduced DATAS (Dynamic Adaptation To Application Sizes) for server GC. Enabled by default, it allows the GC to reduce the number of heaps when the application does not need all of them. This reduces memory usage on machines with many cores but relatively low load.
You can control this with:
{
"runtimeOptions": {
"configProperties": {
"System.GC.DynamicAdaptationMode": 1
}
}
}
A value of 1 enables DATAS (default in .NET 8+). A value of 0 disables it.
Which Should You Choose?
Use workstation GC when:
- Building desktop or mobile applications.
- Running on machines with 1-2 cores (containers with CPU limits).
- Memory is constrained and you want smaller footprint.
Use server GC when:
- Building web APIs or microservices.
- Running on machines with 4+ cores.
- Throughput matters more than memory footprint.
- Handling many concurrent requests.
Watch out for containers. A container with a 1-core CPU limit running server GC still creates one heap per physical core by default, wasting memory. In .NET 8+, the runtime respects container CPU limits. For older versions, set DOTNET_PROCESSOR_COUNT to match your container's CPU limit.
Summary
Workstation GC optimises for low memory and responsiveness. Server GC optimises for throughput on multi-core machines. ASP.NET Core defaults to server GC, which is correct for most web workloads. For containers, verify that the GC respects your CPU limits. And always measure with dotnet-counters before and after changing GC configuration — assumptions about GC behaviour are frequently wrong.