# Appendix B — GC Configuration Reference for Low-Latency .NET (8+)
> Companion to Section 04 (GC Modes & Latency Settings). Section 04 explains the
> mechanisms and tradeoffs; this appendix is the operational reference: every knob
> relevant to an HFT deployment, its exact syntax, default, and our recommendation
> with rationale. Settings are stated for **.NET 8** unless noted; where behavior
> differs in 7/9, it is flagged.
>
> **Verification caveat:** GC configuration names are stable but their *interactions*
> change between runtime releases. Treat the recommendation column as a starting
> point and validate with the Appendix A harness on your exact runtime version.
---
## B.1 The three configuration channels and their precedence
| Channel | Syntax example | When applied | Notes |
|---|---|---|---|
| `runtimeconfig.json` (or `runtimeconfig.template.json` / MSBuild properties) | `"configProperties": { "System.GC.Server": true }` | Process start | Preferred: versioned with the app |
| Environment variables | `DOTNET_gcServer=1` | Process start | Override runtimeconfig; useful for ops experiments. Legacy `COMPlus_` prefix still honored |
| Runtime API | `GCSettings.LatencyMode = ...` | Any time | Only latency mode, LOH compaction mode, and NoGC regions are runtime-switchable |
Boolean env vars use `1`/`0`; several numeric env vars are parsed as **hex** (a
classic operational trap — `DOTNET_GCgen0size=100000` is 1 MiB, not 100,000 bytes).
The `System.GC.*` runtimeconfig values are decimal. Prefer runtimeconfig for this
reason alone.
## B.2 Core mode selection
| Setting | runtimeconfig key | Env var | Default | HFT recommendation |
|---|---|---|---|---|
| Server GC | `System.GC.Server` | `DOTNET_gcServer` | `false` (true for ASP.NET templates) | **`true`.** Per-core heaps, parallel collection, dedicated GC threads (Section 04 §4.2) |
| Concurrent / background GC | `System.GC.Concurrent` | `DOTNET_gcConcurrent` | `true` | **`true`.** Turns Gen2 STW into two short suspension windows (Section 04 §4.4). Disable only for the process-separated control plane where throughput matters more |
| Retain VM | `System.GC.RetainVM` | `DOTNET_GCRetainVM` | `false` | **`true`.** Keeps decommit/recommit churn (and associated page-fault jitter) off the session; memory stays mapped |
## B.3 Heap count and CPU placement (Server GC)
| Setting | runtimeconfig key | Default | HFT recommendation |
|---|---|---|---|
| Heap count | `System.GC.HeapCount` | one per available core | Set to the number of **managed allocating threads**, not machine cores. On a 32-core box running 4 managed hot threads, 4–6 heaps collect far faster than 32 sparse ones |
| Affinitize mask | `System.GC.HeapAffinitizeMask` (hex bitmask) | unset | Place GC heaps/threads on the cores your managed threads own; keep them **off** the kernel-bypass NIC polling cores |
| Affinitize ranges | `System.GC.HeapAffinitizeRanges` (e.g. `"1-4,9"`; on Windows may include CPU groups) | unset | Use instead of mask on > 64-core machines |
| Disable affinity | `System.GC.NoAffinitize` | `false` | Leave `false`; floating GC threads are a jitter source |
| Hard limit | `System.GC.HeapHardLimit` (bytes) / `HeapHardLimitPercent` | container-aware default (75% of limit in containers) | Set explicitly to your sized budget. **Interaction:** the NoGC-region maximum budget is derived from heap size; an over-tight hard limit silently shrinks your usable NoGC window |
| Per-heap-type limits | `System.GC.HeapHardLimitSOH` / `LOH` / `POH` (and `...Percent`) | unset | Use only when fencing a misbehaving LOH; otherwise leave unset |
## B.4 Generation sizing, regions, and DATAS
| Setting | Key / env var | Default | HFT recommendation |
|---|---|---|---|
| Gen0 budget | `DOTNET_GCgen0size` (hex bytes) | dynamic (CPU-cache derived) | Increasing Gen0 size reduces Gen0 GC *frequency* at the cost of slightly longer marks and worse cache locality. For an allocation-free hot path it is irrelevant; for Tier-1-only deployments, a larger Gen0 (e.g. 256 MiB/heap) can push Gen0 GCs out of the session entirely. Measure both |
| Regions vs segments | regions default in .NET 7+ (x64/arm64); fallback to segments via `DOTNET_GCName=clrgc.dll` in some servicing bands | regions | Stay on **regions** (Section 05); only fall back to reproduce a suspected regions-specific regression, and report it upstream |
| DATAS (dynamic heap adaptation) | `System.GC.DynamicAdaptationMode` / `DOTNET_GCDynamicAdaptationMode` (0/1) | off in .NET 8, **on by default in .NET 9** for Server GC | **`0` (off).** DATAS trades steady-state determinism for footprint by resizing heap count mid-run — the opposite of what a latency-bound session wants. This is the single most important new knob to check when moving 8 → 9 |
| Conserve memory | `System.GC.ConserveMemory` (0–9) | 0 | Leave 0. It biases toward compaction/decommit — footprint over latency |
| LOH threshold | `System.GC.LOHThreshold` (bytes, ≥ 85000) | 85000 | Raising it moves "slightly large" arrays into SOH where they compact in ephemeral GCs. Useful targeted fix if profiling shows a band of e.g. 90–120 KB allocations; do not raise wholesale |
| Large pages | `System.GC.HeapHardLimit` + `DOTNET_GCLargePages=1` | off | Eliminates TLB-miss and page-fault jitter on big heaps. Requires OS privilege (`SeLockMemoryPrivilege` / hugepage pool) and a hard limit set; commit-all-up-front semantics — test startup behavior |
| High memory percent | `System.GC.HighMemoryPercent` | 90 | Lower only on shared hosts; on dedicated trading hosts leave default so the GC does not become eager near your normal occupancy |
## B.5 Runtime-switchable controls
```csharp
using System.Runtime;
// 1) Session latency mode — set after warmup, before market open.
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
// Background GC stays enabled; full blocking GCs are avoided except under
// memory pressure or explicit induced GC. See Section 04 §4.5 for what this
// does and does NOT promise.
// 2) Scheduled LOH compaction — maintenance window ONLY (e.g. post-close):
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
// The mode auto-resets to Default after the compacting GC.
// 3) NoGC region around a known critical window:
const long NoGcBudget = 256L * 1024 * 1024; // must fit derived runtime limits
if (GC.TryStartNoGCRegion(NoGcBudget, disallowFullBlockingGC: false))
{
try
{
RunCriticalWindow(); // MUST allocate < NoGcBudget across all heaps
}
finally
{
// EndNoGCRegion throws if a GC already ended the region (budget
// exhausted with disallowFullBlockingGC:false induces a collection).
if (GCSettings.LatencyMode == GCLatencyMode.NoGCRegion)
GC.EndNoGCRegion();
}
}
else
{
// Entry can fail (budget too large for heap configuration, or region
// already active). This branch must be a tested, alarmed code path.
EnterDegradedModeAndAlert();
}
```
Operational rules for NoGC regions (from Section 04 §4.6 and production reports
[R14], [R19]):
1. Size the budget from soak-measured worst-case allocation in the window, ×2.
2. With `disallowFullBlockingGC: true`, exhaustion throws inside your hot path
instead of inducing a GC — pick the failure mode you can actually handle.
3. The budget must fit within ephemeral segment/region capacity as derived from heap
count and hard limits; entry failure on a config change is how this usually
surfaces. Boot-time validation (Section 10 §10.5) should attempt a dry-run region.
## B.6 Adjacent (non-GC) runtime settings that affect GC-visible jitter
| Setting | Why it matters here |
|---|---|
| `true` | Removes most startup JIT; combined with warmup, prevents tier-up recompilation spikes mid-session being misattributed to GC |
| `DOTNET_TieredPGO=0`, `DOTNET_TC_QuickJitForLoops=0`, or `DOTNET_TieredCompilation=0` | Determinism vs peak codegen quality; see Appendix A §A.5 for measurement policy |
| `` / `` MSBuild props | Compile-time equivalents of B.2 keys; pick one channel and stick to it |
| `` / `true>` | Invariant globalization trims ICU allocations/loads at startup; harmless for typical trading payloads (verify if you format non-ASCII) |
| `System.Runtime.TieredCompilation.BackgroundWorkerTimeoutMs` | Tier-up worker scheduling; relevant only if tiering is left on |
## B.7 Reference profile (starting point, to be validated per §A)
`runtimeconfig.template.json` for a hot-path trading process on .NET 8, 6 managed
threads pinned to cores 2–7, 16 GiB heap budget:
```json
{
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true,
"System.GC.RetainVM": true,
"System.GC.HeapCount": 6,
"System.GC.HeapAffinitizeRanges": "2-7",
"System.GC.HeapHardLimit": 17179869184,
"System.GC.DynamicAdaptationMode": 0
}
}
```
Plus, in code at startup (after warmup): `GCSettings.LatencyMode =
GCLatencyMode.SustainedLowLatency;` — and the boot-time validator from Section 10
§10.5 asserting every one of these took effect (`GCSettings.IsServerGC`,
`GC.GetGCMemoryInfo().TotalAvailableMemoryBytes`, heap count via
`GC.GetConfigurationVariables()` on .NET 8+).
> `GC.GetConfigurationVariables()` (returns the GC's view of its own configuration)
> was added in .NET 8 — the cleanest way to assert configuration actually applied;
> verify availability on your target runtime.
---
*End of Appendix B.*