IIS Application Pool Recycling: Schedule, Memory Limits and Diagnosing Leaks

IIS recycles application pools on a schedule by default, but the schedule is the wrong tool for almost every problem it gets used to fix. A recycle does not free native memory leaked by an ISAPI filter, does not flush unmanaged heap held by a COM component, and does not fix a managed leak that the CLR has already rooted in a static collection — it only drops the worker process and starts a new one, which restarts the leak from zero. Understanding what a recycle actually does, and what the exact settings under an IIS host control, is what separates a workaround from a diagnosis.

What a Recycle Terminates and What It Preserves

A recycle terminates the w3wp.exe worker process for that pool and starts a replacement. In-process session state (SessionState mode="InProc") is lost. Output cache is lost. Loaded ISAPI extensions are reloaded. Everything the process held in private bytes returns to the OS. Anything written to disk, to SQL Server, to a distributed cache, or to app-level static state in a separate service survives.

This is why recycling “fixes” a slow site for twenty minutes and then the slowness returns. It also explains the CPU spike after a recycle: the replacement process has a cold JIT, a cold connection pool, and an empty ASP.NET compilation cache if the site is precompiled into the temporary ASP.NET files folder under a path the recycle has invalidated.

The Default Schedule and Where It Lives

By default, application pools recycle every 1740 minutes (29 hours) at a fixed time plus a random offset of up to 7.5 minutes, to prevent every pool on the server from recycling simultaneously. On a busy host this default is reasonable. On a host with a single app and low traffic, the post-recycle cold start is the largest latency event of the day. Change it deliberately rather than reactively.

# Inspect the current recycle configuration
Import-Module WebAdministration
Get-Item IIS:\AppPools\DefaultAppPool | Select-Object -ExpandProperty recycling

# Fixed daily time, no random offset
Set-ItemProperty IIS:\AppPools\DefaultAppPool `
  -Name recycling.periodicRestart.time -Value "1.00:00:00"
Clear-ItemProperty IIS:\AppPools\DefaultAppPool -Name recycling.periodicRestart.schedule
New-ItemProperty IIS:\AppPools\DefaultAppPool `
  -Name recycling.periodicRestart.schedule.collection `
  -Value "03:30:00" | Out-Null
Set-ItemProperty IIS:\AppPools\DefaultAppPool `
  -Name recycling.disallowRotationOnConfigChange -Value $true

disallowRotationOnConfigChange = true is the single most under-applied setting. Without it, any change to web.config — a deployment copying a new file, a config-transformation tool touching a timestamp — triggers an immediate pool recycle. In a deployment pipeline that writes several files, that is several recycles within seconds. On an application with a 20-second warm-up, this alone can produce intermittent 503s during every release window.

Idle Timeout Is a Different Setting Than a Recycle

Idle Timeout (default 20 minutes on IIS 7 through IIS 10) shuts the worker down entirely after that period without requests. The next request gets a cold start. On a public site with bot traffic this is invisible; on an internal application used twice a day, every morning visit pays the full warm-up cost. Set it to 0 to disable, or keep a warm instance and additionally configure startMode="AlwaysRunning" with Application Initialization enabled so the app preloads a real page at pool start.

Set-ItemProperty IIS:\AppPools\AppPool01 -Name processModel.idleTimeout -Value "00:00:00"
Set-ItemProperty IIS:\AppPools\AppPool01 -Name startMode -Value "AlwaysRunning"

# Application Initialization: warm the app after every pool start or recycle
Set-WebConfigurationProperty -Filter "/system.webServer/applicationInitialization" `
  -PSPath "IIS:\Sites\MySite" -Name "remapManagedRequestsTo" -Value "warmup.htm"
Enable-WebGlobalModule -Name "ApplicationInitializationModule"

Why Memory-Based Recycle Thresholds Are a Trap

Private Bytes and Virtual Bytes limits in the pool’s recycling section look like a safety net. In practice they produce a recycle at an arbitrary point, sometimes mid-request, and on a pool running an x64 application the thresholds are almost impossible to calibrate because managed heap growth is not linear. Worse, the CLR has already released the memory the OS can reclaim when it collects; a high Private Bytes count does not equal a leak.

A managed leak shows up as a rising # Gen 2 Collections count relative to time with a Gen 2 heap size that never returns to baseline after GC.Collect(). A native leak shows up as Private Bytes climbing while the managed heap stays flat. Distinguish the two before setting any threshold:

# Managed heap by generation, sampled every 60 s
Get-Counter "\ASP.NET Applications(__Total__)\Gen 2 heap size", `
            "\ASP.NET Applications(__Total__)\% Managed Processor Time", `
            "\Process(w3wp)\Private Bytes", `
            "\Process(w3wp)\Handle Count" -SampleInterval 60 -MaxSamples 30

If Private Bytes climbs but Gen 2 heap is flat, you have a native allocation: check for an ISAPI module (older PDF renderers, legacy authentication filters) or an unmanaged dependency loaded through P/Invoke. If Gen 2 heap climbs and never falls after a full GC, you have a managed leak: capture two memory dumps ten minutes apart with procdump -ma -p w3wp and diff the object counts in WinDbg with !dumpheap -stat. A static Dictionary or a subscribed event handler that is never unsubscribed will be obvious in that diff.

Recycle Events Worth Logging

  • WAS event IDs 5074 (a worker process failed and was replaced) and 5011 (a worker process crashed) indicate a fault, not a scheduled recycle.
  • IIS event 1117 records the reason for an over-limit recycle, including which threshold fired.
  • HTTP.sys event 5137 / 5138 relates to application pool availability during a restart.
  • Application pool “Rapid Fail Protection” (default: 5 failures in 5 minutes) will disable the pool entirely after repeated crashes. Check Get-WebAppPoolState before assuming the site is down for network reasons.

Turn on failed-request tracing for slow requests only (5000 ms threshold) so the trace files do not fill the disk. On a Windows Server VPS with a 60-80 GB volume, the %SystemDrive%\inetpub\logs\LogFiles folder can reach several GB per month on a moderately busy site; configure log rollover and a retention policy rather than discovering it during a patch reboot.

A Workable Policy

Set a single daily recycle at a low-traffic hour, disable rotation on config change, disable idle timeout for always-on apps, and remove memory thresholds unless a specific unmanaged component requires them. Then instrument the actual leak. Tools like the built-in PerfMon counters above and a scheduled dump capture will find the cause in a day; another recycle schedule will not. If you are choosing where to run this workload, the licensing and core-count rules on the blog explain how vCPU allocation maps to pool throughput, and the Windows VPS plans on the main site list RAM and NVMe IOPS, both of which bound how many pools you can realistically run.

Leave a Comment