📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the difference between background GC and the older concurrent GC in .NET, and how do GC latency modes affect pauses?

Concurrent versus background Gen 2 collection, foreground GCs during background GC, configuration naming, latency modes and no-GC regions.

Senior Theory Advanced background-gc concurrent-gc gc latency

🎯 Key points

  • Both concurrent and background GC mark Gen 2 while application threads run
  • Background GC allows foreground Gen 0 and Gen 1 GCs during a background Gen 2
  • Background GC is the default in modern .NET; the setting is still called Concurrent
  • Background GC does not compact; blocking Gen 2 GCs compact when needed
  • Latency modes: Batch, Interactive, SustainedLowLatency, LowLatency, NoGCRegion
  • Use low-latency modes and no-GC regions only for bounded critical periods

⏱ 30-second answer

Both let a Gen 2 collection run mostly while application threads keep working. The older concurrent GC (.NET Framework before 4.0) could not perform ephemeral collections while a concurrent Gen 2 was running, so allocation-heavy threads could block when the ephemeral space ran out. Background GC (workstation from .NET Framework 4.0, server from 4.5, and the default in modern .NET) allows foreground Gen 0 and Gen 1 GCs during a background Gen 2, which removes that stall. Background GC applies only to Gen 2 and does not compact. The setting is still named "concurrent" (<ConcurrentGarbageCollection>, System.GC.Concurrent). Latency modes such as SustainedLowLatency and TryStartNoGCRegion further limit blocking GCs for critical periods.

🗣 Full answer (senior)

How I would answer this in an interview

"Both concurrent GC and background GC do the expensive part of a Gen 2 collection, marking, while application threads keep running. The difference is what happens if the app keeps allocating. With the old concurrent GC, ephemeral collections couldn't run during a concurrent Gen 2, so once Gen 0 filled up, allocating threads stalled until the Gen 2 finished. Background GC, the default in all modern .NET, allows short foreground Gen 0/Gen 1 GCs during the background Gen 2, which removes that stall. Background GC never compacts; when fragmentation or memory pressure is high the GC still does a blocking, compacting Gen 2. Confusingly, the setting is still called Concurrent. On top of that, latency modes like SustainedLowLatency and no-GC regions let you suppress blocking GCs during short, critical windows, but they're last-resort tools."


1. History and naming

Era Gen 2 behaviour Ephemeral GCs during Gen 2?
.NET Framework 1.0–3.5 Concurrent GC (workstation) No: allocations could stall
.NET Framework 4.0 Background GC for workstation Yes
.NET Framework 4.5 Background GC for server too Yes
.NET Core / .NET 5–10 Background GC by default, both modes Yes

The configuration still uses the old name: <ConcurrentGarbageCollection>, System.GC.Concurrent, DOTNET_gcConcurrent. Turning it off gives non-concurrent (all blocking) GCs.

2. How a background GC runs

  1. A Gen 2 GC is needed (budget, LOH allocations, memory load).
  2. Short pause: suspend threads, mark from roots, set up write watch, resume.
  3. Concurrent mark: the background GC thread(s) mark the heap while the app runs and allocates.
  4. If Gen 0 fills meanwhile, a foreground GC (blocking Gen 0 or Gen 1) runs, then the background GC continues.
  5. Short pause: re-scan objects modified during concurrent marking (from write watch), finish marking.
  6. Concurrent sweep: build free lists for dead Gen 2 space. No compaction.

3. Concurrent vs background

Concurrent (legacy) Background (current)
Gen 2 marking concurrent Yes Yes
Gen 0/1 GCs during Gen 2 No Yes (foreground GCs)
Allocation stalls during Gen 2 Possible Largely avoided
Compaction No No (blocking GCs compact)
Server GC support No Yes, background threads per heap

4. What still pauses

Pause Typical length
Background GC start and end Short
Foreground Gen 0/1 GCs Short
Blocking compacting Gen 2 (fragmentation, memory pressure, induced) Long on large heaps

Background GC reduces Gen 2 pause impact; it does not make the GC pause-free.

5. Latency modes

Mode How you get it Effect Use
Batch Concurrent GC disabled All GCs blocking; best throughput Batch jobs
Interactive Default with background GC Normal behaviour Almost everything
SustainedLowLatency GCSettings.LatencyMode = ... Avoids blocking full GCs when possible; background Gen 2 only Bounded critical windows (market hours)
LowLatency GCSettings.LatencyMode = ... (Workstation only) Suppresses Gen 2 GCs except under low memory Very short critical sections
NoGCRegion GC.TryStartNoGCRegion(bytes) No GC at all while allocations stay under the budget Microsecond-sensitive bursts

6. When to change anything

Almost never for ordinary services. The right order:

  1. Confirm with a trace that blocking GCs cause the pauses.
  2. Reduce allocations and fragmentation (pooling, streaming, fewer mid-life objects).
  3. Only then use SustainedLowLatency for a bounded window, or a no-GC region for a short burst with a measured budget.
  4. Restore the previous mode in finally.

Production example

Our [market data service] took a burst of [hundreds of thousands] of messages at the market open and breached its latency SLO for the first minutes, with pauses of [about 300 ms]. A dotnet-trace opened in PerfView GCStats showed blocking compacting Gen 2 GCs at the open, triggered by fragmentation from per-message byte[] buffers. I pooled the message buffers first, which removed most fragmentation, and then set GCSettings.LatencyMode = SustainedLowLatency during trading hours only, restoring Interactive after the close so compaction could happen overnight. Blocking GCs during the open dropped to [zero] and the SLO held; we added a dashboard for blocking GC count during trading hours.

🔍 What the interviewer is checking

The interviewer checks detailed GC knowledge for latency-sensitive systems.

Green flags: concurrent versus background difference, foreground GCs, no compaction in background, configuration naming, latency modes with safe usage, trace-based verification.

Red flags: thinks background GC is pause-free; sets low-latency modes permanently; confuses the terms with no explanation.

📚 Learn in depth

1. Timeline of a background GC with a foreground GC

app threads   ====|p|=====================|g0|=========================|p|=====
BGC thread         [start][  concurrent mark ......... ][final mark][  sweep  ]
foreground                                |g0|
 p = short pause    g0 = foreground (blocking) Gen 0 GC during the BGC

With the legacy concurrent GC, the g0 would not be allowed: a thread that ran out of Gen 0 space would wait until the concurrent Gen 2 finished.

2. How concurrent marking stays correct

While the background thread marks, the application keeps writing references. If the app stores a reference to an unmarked object into an already-scanned object, the GC could miss it. The runtime records modified memory using write watch (implemented in software via the write barrier in modern .NET). In the final pause, the GC re-scans modified pages/cards so nothing live is missed.

That is also why background GC has a small ongoing cost: the write barrier variant used while a BGC is in progress does extra work.

3. Why background GC can't compact

Compaction moves objects and rewrites references. Doing that while app threads run would need read barriers (check-and-forward on every reference load) or a stop-the-world phase. .NET chose not to put read barriers on every load for performance, so concurrent work is limited to marking and sweeping. When fragmentation matters, the GC decides to do a blocking compacting Gen 2 instead.

4. Background GC with Server GC

  • Each heap has a background GC thread in addition to its foreground GC thread.
  • Foreground ephemeral GCs during a BGC run in parallel across heaps.
  • With DATAS (.NET 9+), the number of heaps, and therefore GC threads, adapts to load.

5. No-GC regions in detail

const long budget = 32 * 1024 * 1024;
if (GC.TryStartNoGCRegion(budget))
{
    try
    {
        RunMatchingBurst();   // must allocate less than the budget
    }
    finally
    {
        if (GCSettings.LatencyMode == GCLatencyMode.NoGCRegion)
            GC.EndNoGCRegion();
    }
}
  • TryStartNoGCRegion may perform a GC first to make the space available.
  • There is a maximum budget (related to ephemeral space size); large values throw ArgumentOutOfRangeException.
  • Exceeding the budget silently ends the region (a GC can then occur); EndNoGCRegion would then throw, hence the check.
  • .NET 8+ adds GC.RegisterNoGCRegionCallback to be notified if the budget is exhausted.
  • You cannot set GCSettings.LatencyMode = NoGCRegion directly.

6. Seeing GC types in traces

PerfView GCStats "Type" Meaning
BackgroundGC Background Gen 2
ForegroundGC Ephemeral GC during a background GC
NonConcurrentGC Blocking GC (ephemeral or full)

In-process: GC.GetGCMemoryInfo(GCKind.Background), GCKind.FullBlocking, GCKind.Ephemeral return the most recent GC of each kind, with pause durations and whether it compacted.

7. Version notes

Version Change
.NET Framework 4.0 Background workstation GC
.NET Framework 4.5 Background server GC; SustainedLowLatency
.NET Framework 4.6 GC.TryStartNoGCRegion
.NET 5 GC.GetGCMemoryInfo(GCKind)
.NET 8 GC.RegisterNoGCRegionCallback
.NET 9 / 10 DATAS adapts background GC threads with heap count

8. Try it yourself

  1. Run the code example and confirm the default mode is Interactive.
  2. Run with DOTNET_gcConcurrent=0 and confirm Batch; compare GC.GetGCMemoryInfo(GCKind.Background).Index (stays 0).
  3. Allocate and retain ~500 MB of small objects in a loop while timing a separate thread's iterations; compare max iteration latency with concurrent on and off.
  4. Try GC.TryStartNoGCRegion with a budget smaller than what you allocate and observe the mode change and the exception if you call EndNoGCRegion unconditionally.
  5. Capture dotnet-trace collect --profile gc-collect under load and find ForegroundGC entries occurring inside a BackgroundGC in PerfView.

💻 Code

using System.Runtime;

Console.WriteLine($"Default latency mode: {GCSettings.LatencyMode}");  // Interactive with background GC

// Critical period: avoid blocking full GCs where possible
GCLatencyMode previous = GCSettings.LatencyMode;
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
try
{
    ProcessMarketOpen();
}
finally
{
    GCSettings.LatencyMode = previous;
}

// Very short burst with a bounded allocation budget
if (GC.TryStartNoGCRegion(16 * 1024 * 1024))
{
    try
    {
        var batch = new byte[64][];
        for (int i = 0; i < batch.Length; i++) batch[i] = new byte[64 * 1024];
        Console.WriteLine($"Allocated in no-GC region, mode: {GCSettings.LatencyMode}");
    }
    finally
    {
        if (GCSettings.LatencyMode == GCLatencyMode.NoGCRegion)
            GC.EndNoGCRegion();
    }
}

GCMemoryInfo bg = GC.GetGCMemoryInfo(GCKind.Background);
Console.WriteLine(bg.Index == 0 ? "No background GC yet" : $"Last background GC #{bg.Index}, gen {bg.Generation}");

static void ProcessMarketOpen()
{
    for (int i = 0; i < 1_000_000; i++) _ = new byte[128];
}

⚠ Common mistakes

1. Thinking background GC eliminates pauses

What happens: "we use background GC, so GC can't cause latency". Why it is wrong: start/end pauses, foreground ephemeral GCs and blocking compacting Gen 2 GCs still occur. Do instead: say it reduces Gen 2 pause impact, and measure pause types.

2. Confusing the setting name with the old algorithm

What happens: "we turned on concurrent GC" is read as using the legacy algorithm. Why it is wrong: the Concurrent setting enables background GC in modern .NET. Do instead: explain the naming history.

3. Disabling concurrent GC for "performance" in services

What happens: ConcurrentGarbageCollection=false in an API. Why it is wrong: every Gen 2 becomes fully blocking, increasing tail latency. Do instead: keep it on for interactive/request workloads; turn off only for batch jobs.

4. Leaving SustainedLowLatency or LowLatency on permanently

What happens: the mode is set at startup and forgotten. Why it is wrong: compaction is avoided, so fragmentation and memory grow until the GC is forced to do something drastic. Do instead: bound the window and restore the previous mode in finally.

5. Using LowLatency with Server GC

What happens: code sets GCLatencyMode.LowLatency in an ASP.NET Core app. Why it is wrong: it isn't available for Server GC and is meant for short Workstation critical sections. Do instead: SustainedLowLatency or a no-GC region.

6. Calling EndNoGCRegion unconditionally

What happens: the budget was exceeded and the region already ended. Why it is wrong: EndNoGCRegion throws InvalidOperationException. Do instead: check GCSettings.LatencyMode == GCLatencyMode.NoGCRegion first.

7. Guessing the no-GC budget

What happens: a large arbitrary budget is requested. Why it is wrong: too large throws; too small silently ends the region. Do instead: measure allocation of the critical section with GC.GetAllocatedBytesForCurrentThread() and add margin.

8. Changing latency modes without traces

What happens: modes are toggled because "GC is slow". Why it is wrong: the pause may be ephemeral GCs, thread pool starvation or I/O. Do instead: confirm blocking GCs with a trace first.

9. Expecting background GC to fix fragmentation

What happens: fragmentation keeps growing despite many background GCs. Why it is wrong: background GC only sweeps; it doesn't compact. Do instead: fix the fragmenting pattern; compaction comes from blocking GCs.

10. Treating latency modes as a substitute for reducing allocations

What happens: low-latency mode is used to hide a high allocation rate. Why it is wrong: the cost moves elsewhere (memory growth, later longer GCs). Do instead: reduce allocations and mid-life objects first.

🏛 Senior insight: trade-offs

Latency tuning is a last step after reducing allocations and fragmentation.

Need Tool
General service Defaults (background GC, Interactive)
Bounded critical window (market open) SustainedLowLatency for that window
Microsecond-sensitive burst TryStartNoGCRegion with measured budget
Batch throughput Batch (non-concurrent)

Seniors confirm with traces that blocking GCs are the actual cause of pauses before changing modes.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says blocking full GCs still happen Why: Checks Gen 2 investigation
    How to answer

    A Gen 2 GC collects the whole managed heap: Gen 0, Gen 1, Gen 2, the LOH and the POH. It is triggered when Gen 2's budget is exceeded by promotions, when large object allocations exceed the LOH budget, under memory pressure (approaching the container or GCHeapHardLimit), or by code calling GC.Collect(). Most Gen 2 GCs run as background GCs that let threads keep running; blocking full GCs (with compaction) pause everything. To investigate frequent Gen 2 GCs, check the GC reason (dotnet-trace or PerfView GCStats), then the cause: LOH churn, mid-life objects, induced collections, or memory limit pressure.

  2. When: Candidate mentions the Concurrent setting for workstation Why: Checks Workstation GC modes
    How to answer

    Workstation GC uses a single heap and runs collections on the thread that triggered them (with a background GC thread for concurrent Gen 2 work). It aims for small memory footprint and short pauses rather than maximum throughput. It is the default for console, desktop and worker apps that do not use the Web SDK; ASP.NET Core apps default to Server GC. You enable it with <ServerGarbageCollector>false</ServerGarbageCollector>; background (concurrent) GC is on by default and can be turned off with <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>. Choose Workstation GC for desktop apps, CLI tools, and many small containers where memory density matters more than throughput.

  3. When: Candidate mentions background GC threads per heap Why: Checks Server GC
    How to answer

    Server GC creates one heap per logical CPU (limited by affinity and container CPU limits) with dedicated, high-priority GC threads that collect in parallel. Budgets are larger, so GCs are less frequent and throughput is high, but memory use is higher. It is the default for ASP.NET Core. DATAS (Dynamic Adaptation To Application Sizes), opt-in in .NET 8 and on by default with Server GC since .NET 9, starts with fewer heaps and adjusts the heap count and budgets to the live data size and load, so a lightly loaded service uses far less memory. In containers, the GC honours CPU and memory limits; GCHeapHardLimit (or ...Percent), GCHeapCount and GCConserveMemory give finer control.

🛠 Real-world problems: how you handle them

Blocking full GCs at market open removed with SustainedLowLatency and pooling
Situation
A market data service handled a burst of [hundreds of thousands] of messages at the market open.
Signals
[300 ms] pauses at the open; latency SLO breaches for the first minutes.
Investigation
PerfView GCStats showed blocking compacting Gen 2 GCs caused by fragmentation from message buffers.
Resolution
Pooled message buffers and set `SustainedLowLatency` during trading hours, restoring `Interactive` after close.
Prevention
Dashboards for blocking GC count during trading hours.

How to tell it (STAR)

In [our market data service], the open caused [300 ms] GC pauses. Traces showed blocking compacting GCs, so I pooled the buffers that fragmented the heap and used SustainedLowLatency only during trading hours. Blocking GCs at the open disappeared.

More in Garbage Collection

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me