IIS Worker Process Hangs: Ping Health, Rapid-Fail Protection and Crash Dumps

An IIS worker process that hangs usually does not crash. The site stops responding, the request queue grows, and Event Viewer shows nothing because no exception was thrown — the thread pool is simply blocked. IIS detects exactly this, but the useful settings are not the defaults, and configuring pingingEnabled and rapidFailProtection correctly turns an invisible hang into a logged event with a call stack.

This applies to IIS 10 on Server 2016 through 2025, for classic ASP.NET and for ASP.NET Core via the ASP.NET Core Module. The mechanism is the same; only the dump format differs.

How IIS decides a worker process is hung

With pingingEnabled="true" on the pool, WAS (Windows Process Activation Service) pings w3wp.exe on a fixed interval. If the process misses the timeout repeatedly, WAS marks the worker unhealthy and, depending on pingFailureLimit, terminates it or stops the pool.

Pings run on a dedicated native thread, not the managed request pipeline, so a hang in application code does not block them — which is why this detection works where a health-check URL would also hang. A process genuinely starved of CPU will still stop answering pings, giving false positives on oversubscribed VPS instances.

Defaults are conservative: pinging is on with a 30-second period, and rapid-fail protection shuts the pool after five failures in five minutes. That last setting causes outage cascades, because nothing restarts a shut-down pool and the site returns 503 until an administrator intervenes.

Setting the ping parameters deliberately

Configure through PowerShell so the settings are reproducible, using the WebAdministration module that ships with IIS:

Set-ItemProperty IIS:\AppPools\MyApp -Name processModel.pingingEnabled -Value $true, then Set-ItemProperty IIS:\AppPools\MyApp -Name processModel.pingInterval -Value 00:00:30 and Set-ItemProperty IIS:\AppPools\MyApp -Name processModel.pingResponseTimeout -Value 00:01:30. Verify with Get-ItemProperty IIS:\AppPools\MyApp -Name processModel | Select-Object pingingEnabled, pingInterval, pingResponseTimeout, pingFailureLimit.

The pingResponseTimeout needs care. A GC pause, a first-request JIT storm or a synchronous read over slow storage legitimately takes 10 to 20 seconds, so a 5-second timeout gives false positives on cold starts. A 90-second timeout with a 30-second interval is a good baseline for a 4 GB VPS; 120 seconds on heavier apps.

  • Classic ASP.NET with SQL Server on the same box: 90 s timeout, 30 s interval.
  • ASP.NET Core 8/10 with EF Core cold start: 120 s timeout.
  • Server Core with minimal RAM (2 GB): raise rapid-fail threshold before anything else.
  • Never set pinging off to silence alerts — you lose the only native hang detection you have.

Capturing the stack when a ping failure happens

Enable automatic dumps so the termination produces something you can analyse. In the application pools processModel or under system.webServer/httpErrors that is not enough; you need the crash-dump registry keys or the Windows Error Reporting LocalDumps feature. Set HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\w3wp.exe with DumpFolder pointing at a folder with write access for the IIS_IUSRS group, and DumpType set to 1 (mini) or 2 (full).

A mini dump is usually enough for a stack trace and far easier to move off a VPS. In WinDbg, load .loadby sos clr for classic ASP.NET, then run !clrstack -all and !threads to see blocked frames. For ASP.NET Core, dotnet-dump analyse opens the same file.

The usual stack is a synchronous call to an external HTTP or database endpoint inside a lock. If 200 threads are parked on SqlDataReader.Read or HttpClient.Send, the hang is in the application and raising the ping timeout only delays discovery.

Rapid-fail protection: friend and enemy

Rapid-fail protection stops a crashing app from restarting in a loop and eating all the CPU, but it also converts a transient failure into a full outage. Five failures in five minutes is aggressive for an app that fails once at startup against a slow database and then recovers.

Tune it explicitly: Set-ItemProperty IIS:\AppPools\MyApp -Name failure -Value @{rapidFailProtection=$true; rapidFailProtectionInterval="00:10:00"; rapidFailProtectionMaxCrashes=10}. Note that setting failure as a hashtable replaces the whole collection, so read the current value first with Get-Item IIS:\AppPools\MyApp | Select-Object -ExpandProperty failure and preserve any custom orphanActionExe or orphanWorkerProcess entries.

When rapid-fail does trip, the event log records Event ID 5002 (application pool failed) and Event ID 5011 (application pool disabled). Monitor both. A 5011 on a production box means the site is down until someone re-enables the pool; Start-WebAppPool -Name MyApp recovers it, but it will not fix the underlying crash.

Distinguishing a hang from a memory leak

A pool that recycles every few hours with no errors is usually leaking, not hanging. Check Get-Counter "\Process(w3wp*)\Private Bytes" over 24 hours: a sawtooth climbing between recycles is a leak; a flat line followed by a jump and a ping timeout is a deadlock.

For leaks, take a dump while the process is healthy. In dotnet-dump, dumpheap -stat shows the type with the highest retained size; WinDbg with SOS gives !dumpheap -stat. The type growing monotonically across two dumps twenty minutes apart is the leak, and dotnet-counters shows it live without a dump.

Health endpoints versus IIS pings

A ping tells you the process is alive and scheduled. A health endpoint tells you the application can reach its database and dependencies. They answer different questions, and you need both. IIS pings catch thread starvation and deadlocks; an HTTP health check catches a process that is running perfectly but cannot reach SQL Server.

In ASP.NET Core, add AddDbContextCheck<AppDbContext>() to AddHealthChecks(), map it with app.MapHealthChecks("/healthz"), and have your monitor hit it every minute. Keep the path out of the sitemap so you are not announcing it to scanners.

What to log so the next incident is shorter

Record the app pool name and w3wp process ID with every request. Enabling the ProcessId field in IIS W3C logging makes correlating an event log entry to a specific request trivial, and appcmd list config -section:system.applicationHost/sites shows what each site currently logs.

At minimum, alert on Event IDs 5002, 5011 and 5138 (worker process disabled), plus any pool restart event. Those three IDs cover rapid-fail shutdowns, isolated crashes and disabled pools. Our reference on IIS application pool configuration and recycling explains the related overlap-recycle settings, and our guide to sizing a Windows server for IIS and SQL workloads covers how much RAM to leave for the ping thread and the dumper under load.

The goal is not to eliminate hangs; it is to make each one produce a dump, an event, and a page before a customer notices. That takes about twenty minutes of PowerShell and two registry keys.

Leave a Comment