Skip to Content
Course content

87: Server GC vs Workstation GC

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You’ve probably spent most of your time in C# focusing on what you're allocating, but rarely on how the runtime decides to clean it up. For a long time, the default settings just "work." But once you move from a desktop utility to a high-traffic API, or from a microservice to a massive data-processing engine, you'll hit a wall where the Garbage Collector (GC) becomes your biggest bottleneck. I've seen developers spend weeks optimizing LINQ queries only to realize the real culprit was that they were running a high-throughput server on Workstation GC settings.

The trap of the "responsive" collector

By default, if you're running a client application, .NET uses Workstation GC. It's designed for a very specific user experience: responsiveness. The goal here is to avoid those annoying "stutters" in a UI. To achieve this, the GC tries to keep pause times short, often performing collections on a single thread or using background threads that don't hog the CPU. It's great for a Windows Forms app or a small tool, but it's a disaster for a server handling 10,000 requests per second.

If you leave a high-load API on Workstation GC, you'll notice a strange phenomenon. Your CPU usage might look low—maybe only 20%—but your request latency is spiking. This happens because the GC is struggling to keep up with the allocation rate on a single heap. It's like trying to empty a swimming pool with a bucket while a fire hose is filling it up. Eventually, the GC has to stop everything for a "full" collection, and your API just freezes for a few hundred milliseconds. In a microservices architecture, that's enough to trigger a timeout in a calling service and start a cascading failure.

Trading latency for raw throughput

This is where Server GC comes in. When you flip the switch to Server GC, .NET changes its entire strategy. Instead of one heap, it creates a separate heap and a separate GC thread for every logical processor on your machine. If you're on a 16-core server, you now have 16 heaps working in parallel.

The trade-off is a classic engineering compromise: throughput vs. latency. Server GC is designed to move as much memory as possible, as fast as possible. It doesn't care as much about a tiny stutter in a UI because there is no UI. It cares that the total amount of time the CPU spends cleaning memory is minimized so that more cycles can go toward processing requests. However, Server GC is "hungrier." It will claim more memory from the OS and hold onto it longer than Workstation GC would. If you run a tiny console app in Server GC mode on a massive machine, you'll be baffled why a "Hello World" program is suddenly eating 500MB of RAM.

Deciding which lever to pull

I usually tell my team to follow a simple rule of thumb: if it's a service that lives in a container or on a server and is expected to handle concurrent load, use Server GC. If it's a tool that runs on a user's laptop, stick with Workstation.

You can control this in your .csproj file or your runtimeconfig.json. The naive approach is to just leave it to the environment, but being explicit prevents surprises when you move from a 4-core dev machine to a 64-core production cluster.

<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

Just be careful. If you're deploying to a Kubernetes cluster with tight memory limits (say, 256MB), Server GC can actually be a liability. Because it allocates multiple heaps, it can hit that memory limit much faster than Workstation GC, leading to an OOM (Out of Memory) kill by the orchestrator. In those specific "tight-memory" cloud scenarios, I've actually gone back to Workstation GC or tuned the gcHeapHardLimit to keep the runtime from getting too greedy.




📋 Practical Task

Configuring a High-Throughput Telemetry Processor

You are building a telemetry ingestion service that processes millions of small objects per minute. Currently, the service is running with default settings on a 16-core machine, but you're seeing significant "Stop-the-World" pauses that are causing request timeouts.

Your Task:

  1. Modify the project configuration to explicitly enable Server GC.
  2. Assuming this service will be deployed to a Docker container with a strict memory limit of 1GB, add the configuration setting to the .csproj or runtimeconfig.json that prevents the GC from exceeding a specific heap hard limit (set it to 800MB to leave room for the OS).
  3. Write a brief comment in your file explaining why you chose Server GC over Workstation GC for this specific telemetry use case.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.