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:

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:

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:

MyApp.csproj
<PropertyGroup>
    <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

Or in runtimeconfig.json:

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.

MyApp.csproj
<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:

You can also use GC.GetGCMemoryInfo() programmatically:

Example.cs
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:

runtimeconfig.json
{
  "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:

Use server GC when:

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.