Most ASP.NET Core 10 sites on IIS never touch GC settings. That works until traffic spikes, the working set climbs, and the app pool hits a private-memory recycle mid-request. The punchline is simple: IIS recycle limits and the .NET garbage collector must agree on a budget, or you get thrash instead of recovery.
On Windows hosting you control two levers—the app pool’s memory recycle thresholds and the runtime’s heap hard limit. Set only one and you still wake up to 503s. Set them together and long-running .NET apps stay predictable under IIS.
#What IIS actually recycles
IIS Application Pools expose Private Memory Limit and Virtual Memory Limit (KB). When the worker process crosses the private limit, WAS recycles w3wp.exe. That is not a gentle GC. In-flight requests die, Data Protection keys must reload if they are not persisted, and the next request pays a cold start—even with preload enabled.
For ASP.NET Core (in-process with the ASP.NET Core Module), the CLR lives inside that same w3wp. If the managed heap can grow past the IIS private limit before GC reacts, recycle wins and your tuning never runs. Cap the heap below the pool limit so GC pressure comes first.
#Server GC is the right default—confirm it
Web workloads on multi-core Windows boxes should run Server GC. ASP.NET Core published for IIS normally gets that via the runtimeconfig generated at publish. Still verify on the server after deploy; a mistaken Workstation GC setting shows up as high pause sensitivity under concurrent requests rather than steady throughput.
# On the IIS box, after the app has handled traffic:
Get-Process w3wp | Select-Object Id, WorkingSet64, PrivateMemorySize64
# Optional: confirm GC mode from a diagnostics dump or dotnet-counters
# dotnet-counters monitor System.Runtime -p <pid>
Watch Working Set versus Private Bytes while you load a representative page that hits SQL Server. If Private Bytes stair-steps upward across requests with rare gen-2 collections, you need an explicit heap ceiling—not another recycle.
#Set a heap hard limit under the pool ceiling
.NET exposes DOTNET_GCHeapHardLimit (absolute bytes, hex) and DOTNET_GCHeapHardLimitPercent (percent of physical RAM). On shared or reseller-style Windows hosts, prefer an absolute limit so noisy neighbors and total machine RAM do not change your app’s behavior. Keep the hard limit roughly 70–80% of the app pool private memory limit so GC collects before WAS recycles.
Example: pool private memory limit 1,024,000 KB (~1 GB). Set a hard limit near 800 MB so gen-2 runs while the process is still legal under IIS.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
hostingModel="inprocess">
<environmentVariables>
<!-- 0x32000000 = 800 MB heap hard limit -->
<environmentVariable name="DOTNET_GCHeapHardLimit" value="0x32000000" />
<environmentVariable name="DOTNET_GCServer" value="1" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>
Apply the same values through the IIS Configuration Editor or your publish transform so Web Deploy does not wipe them. Hex values are easy to misread in tickets—document the decimal MB equivalent next to the setting in your runbook.
#Align SQL and request behavior with the budget
GC tuning fails if the app pins large graphs every request. Typical IIS-hosted offenders: unbounded EF Core tracking on big read queries, loading full result sets into memory for CSV export, and per-request MemoryStream copies of uploads. Prefer AsNoTracking for read paths, page SQL results, and stream file responses when payloads are large.
- Match pool private memory limit and DOTNET_GCHeapHardLimit (limit > heap).
- Keep Server GC on for multi-core IIS workers; do not force Workstation GC “to save memory” on web apps.
- Avoid recycling on Virtual Memory alone—private bytes track the pressure you care about.
- After changes, load-test with dotnet-counters (gc-heap-size, time-in-gc) and watch for recycles in System event logs.
Practical takeaway: pick a private-memory recycle ceiling for the IIS app pool, set DOTNET_GCHeapHardLimit about 20–30% lower in web.config, confirm Server GC, then retest under realistic SQL-backed traffic. When GC is allowed to reclaim before WAS kills w3wp, ASP.NET Core 10 on Windows stops trading stability for surprise cold starts—and you keep the network uptime SLA meaningful for the apps that sit on top of it.
Comments
No comments yet