🌐 IIS

IIS Application Pool Management

📅 July 21, 20269 min readITVedas

How IIS application pools isolate worker processes, and how to configure identities, recycling, idle timeout, preload, and pipeline mode correctly.

INTERMEDIATE
⏱ 9 min read
Prerequisites:
Key Facts
  • An application pool's worker process (w3wp.exe) is the real isolation boundary — pools share nothing, so one pool crashing does not affect sites running in other pools
  • ApplicationPoolIdentity is the default identity in IIS 7.5+ and is more secure than NetworkService because each pool gets its own auto-generated virtual account and SID
  • The default periodic restart interval is 1740 minutes (29 hours), deliberately offset from 24 hours so scheduled recycles don't collide at the same time every day
  • Rapid-fail protection stops a pool automatically after 5 worker process crashes within a 5-minute window by default, returning 503 instead of crash-looping

What an Application Pool Isolates

An application pool's real substance is a worker process — w3wp.exe — spawned by the Windows Process Activation Service (WAS) when the first request for that pool arrives (or immediately, if the pool is set to start eagerly). Every application assigned to a pool runs inside that one process, sharing its address space, its AppDomain(s) for classic .NET, its threads, and its memory. Applications assigned to a different pool get a completely separate w3wp.exe with its own memory space and its own thread pool.

This is why pool assignment matters more than most admins treat it: if two sites share a pool and one leaks memory or deadlocks its request-processing threads, both sites degrade together, because they're competing for the same process's resources. Split them into separate pools and a crash, hang, or memory bloat in one is contained — WAS restarts the failed process, the other pool's w3wp.exe never notices. This is also why per-pool CPU and memory limits (configurable under cpu and recycling.periodicRestart in applicationHost.config) only make sense as a per-pool control — they throttle everything sharing that process together, not individual applications within it.

A single pool can host multiple IIS "applications" (each with its own path and content root), and in that case isolation between those applications is weaker than people assume — they still share the worker process's memory and app domain (in Classic .NET) or the same .NET runtime instance (in .NET Core/5+ out-of-process hosting). If you need genuine fault isolation between two applications, they need separate pools, not just separate paths under the same pool.

You can inspect the current worker processes and which pools they belong to directly:

Import-Module WebAdministration
Get-ChildItem IIS:\AppPools | Select-Object Name, State

# Or via appcmd, showing live worker processes and their pool
appcmd list wp

Application Pool Identities

The identity a pool runs under determines what the worker process — and therefore every application in it — can touch on disk, over the network, and in the Windows security model. IIS gives you four practical choices.

ApplicationPoolIdentity is the default since IIS 7.5 and should be the default choice today. When you create a pool, IIS generates a virtual account named IIS APPPOOL\<PoolName> with its own SID, adds it to the built-in IIS_IUSRS group, and automatically grants that account read (and, for the content root, sometimes write) NTFS permissions on the site's physical path. No password to manage, no account to provision in Active Directory, and — critically — no shared credential between pools. If pool A's identity is compromised, it has no bearing on pool B's ACLs, because they're different SIDs entirely.

NetworkService runs the worker process using the computer's domain machine account credentials. It has broader network reach than a virtual account (it authenticates to other machines as the computer account) but every pool configured to use it shares the exact same identity — a compromise or a bad ACL grant on one NetworkService-run pool effectively applies to all of them. It's a legacy option kept for backward compatibility; there's rarely a reason to choose it over ApplicationPoolIdentity on a current IIS version.

Custom service accounts (domain accounts like CORP\svc-webapp01) are still necessary when the application needs to authenticate to a remote resource using Windows/Integrated authentication — a SQL Server that only trusts domain accounts, a UNC file share on another server, or a resource protected by Kerberos delegation. ApplicationPoolIdentity's virtual account has no presence in Active Directory, so it can't authenticate off-box under Windows auth without extra plumbing (like granting the machine account access, which conflates back to NetworkService-style sharing). When you do use a custom account, it needs the "Log on as a service" right and explicit NTFS ACLs on the content path — none of that is automatic the way it is for ApplicationPoolIdentity.

LocalSystem/LocalService should be avoided outright — LocalSystem in particular has full control of the machine, which is far more privilege than a web application process ever needs.

Setting a custom identity via PowerShell:

Import-Module WebAdministration

Set-ItemProperty -Path "IIS:\AppPools\MyAppPool" -Name processModel `
  -Value @{ userName = "CORP\svc-webapp01"; password = "P@ssw0rd!"; identityType = 3 }

# identityType: 0=LocalSystem, 1=LocalService, 2=NetworkService, 3=SpecificUser, 4=ApplicationPoolIdentity

Recycling: Scheduled, Memory-Based, and the Leak It Hides

Recycling replaces the worker process with a fresh one without dropping the pool itself — WAS starts a new w3wp.exe, and once it's ready, the old one is torn down. IIS supports several triggers, configured under recycling.periodicRestart on the pool.

Scheduled/interval recycling is the default: periodicRestart.time is set to 1740 minutes (29 hours) out of the box, intentionally not 24 hours so the restart time drifts instead of landing at the same clock time — and therefore the same traffic pattern — every single day. You can instead set specific daily times via periodicRestart.schedule, which is usually preferable because you control exactly when the brief disruption happens (e.g., 3 AM instead of whenever the 29-hour timer expires mid-afternoon).

Memory-based recycling triggers when the process's private memory (periodicRestart.privateMemory, in KB) or virtual memory (periodicRestart.virtualMemory) crosses a configured threshold. This is the setting people reach for when a process's memory footprint climbs over time.

Here's the trap: if an application has a genuine memory leak, a nightly scheduled recycle (or a memory-threshold recycle that fires reliably before the leak causes real trouble) will make the symptom disappear from monitoring. The process never gets old enough to visibly balloon, so nobody investigates. The leak is still there — you've just capped its blast radius by periodically discarding the evidence. That's a reasonable stopgap for production stability, but it should never be the terminal fix. If you find yourself tuning privateMemory lower and lower to keep a pool "healthy," that's a strong signal to profile the app (dotnet-trace, dotMemory, or a memory dump analyzed with WinDbg/SOS) rather than tightening the recycle threshold again.

By default, recycling is overlapped — the outgoing worker process is kept alive just long enough to finish in-flight requests while the new process spins up and starts accepting new ones, avoiding dropped connections. Setting disallowOverlappingRotation to true disables this and stops the old process immediately, which you generally don't want except on memory-constrained boxes where running two worker processes simultaneously, even briefly, is untenable.

# Set a specific recycle time instead of the rolling interval
$pool = Get-Item IIS:\AppPools\MyAppPool
$pool.recycling.periodicRestart.time = [TimeSpan]::Zero
$pool.recycling.periodicRestart.schedule.Add(@{value = "03:00:00"})
$pool | Set-Item

# Recycle when private memory exceeds 1 GB (value is in KB)
Set-ItemProperty -Path "IIS:\AppPools\MyAppPool" -Name recycling.periodicRestart.privateMemory -Value 1048576

Idle Timeout, Always On, and Preload

By default, a pool's worker process shuts down after processModel.idleTimeout of no requests — 20 minutes out of the box — to free up memory and CPU on the box. The cost shows up on the next request after idle: IIS has to spin up a new worker process, .NET has to JIT the application's assemblies (or, for ASP.NET Core, initialize the host and DI container), and any expensive startup work — warming caches, opening connection pools, compiling Razor views — runs synchronously in front of that unlucky first user. On a low-traffic internal tool that's a fair trade. On anything customer-facing, that cold-start spike is a real, measurable latency problem, and the same cold start happens after every recycle too, not just after idle shutdown.

Two settings address this, and they need to be used together to actually eliminate cold starts:

Without preloadEnabled, AlwaysRunning only gets you a process that exists — the application inside it is still cold until the first real request touches it. Without AlwaysRunning, the pool still shuts down on idle timeout and preload only helps on the next scheduled start, not on-demand. You generally want both, plus idleTimeout raised or disabled (set to 00:00:00) so the pool doesn't keep tearing itself down between bursts of traffic.

IIS 8+ also offers idleTimeoutAction = Suspend instead of the default Terminate: rather than killing the process on idle, IIS suspends it (paging it out) and resumes it on the next request — faster than a full cold start, though still not instant.

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

# Enable preload on the application (requires the Application Initialization feature)
Set-ItemProperty -Path "IIS:\Sites\MySite\MyApp" -Name preloadEnabled -Value True

Integrated vs. Classic Pipeline Mode

The managedPipelineMode setting decides how a pool routes requests through managed (.NET) code relative to IIS's native request pipeline.

Integrated mode, the default since IIS 7 and the only mode that makes sense for anything current, unifies the two pipelines: native IIS modules and managed HttpModules participate in the same set of pipeline events (BeginRequest, AuthenticateRequest, ExecuteRequestHandler, and so on), for every request regardless of file extension. Managed authentication and authorization modules can inspect and act on requests for static files, other-language handlers, anything — not just requests IIS decides to hand off to ASP.NET.

Classic mode reproduces the IIS 6 model: ASP.NET runs as an ISAPI extension sitting behind the native pipeline, and only requests IIS has mapped to that ISAPI extension (historically by file extension, like .aspx) ever reach managed code. It's a second, separate pipeline bolted onto the native one rather than a merged one, which means managed modules don't see requests for content IIS handles natively, and the request lifecycle events don't line up exactly with Integrated mode's.

Classic mode exists purely for compatibility with applications built against that older model: legacy apps with custom ISAPI filters or extensions, or managed code written against assumptions that only hold under the old wildcard-mapped pipeline. New applications should never be started in Classic mode, and ASP.NET Core apps are irrelevant to this setting in practice — they run out-of-process (or in-process via ANCM) behind Kestrel/HTTP.sys logic, and the pool hosting them should simply stay on the Integrated default.

Set-ItemProperty -Path "IIS:\AppPools\LegacyAppPool" -Name managedPipelineMode -Value "Classic"

Rapid-Fail Protection and Everyday Management

Rapid-fail protection guards against crash loops: if a pool's worker process crashes failureInterval's worth of times — 5 failures in 5 minutes by default — WAS stops the pool entirely and every request against it returns 503 Service Unavailable instead of continuing to spin up processes that keep dying. That's a deliberate trade: a hard, visible outage instead of a resource-consuming crash loop that could take the rest of the box down with it. Tune failureInterval and maxFailures under failure on the pool if 5-in-5 is too aggressive or too lax for a given app's crash characteristics, and check logEventOnRecycle to make sure recycle reasons are actually landing in the event log for diagnosis.

Day-to-day, a handful of cmdlets cover most operational needs:

Import-Module WebAdministration

# List pools and current state
Get-ChildItem IIS:\AppPools | Select-Object Name, State

# Start / stop / restart a specific pool
Start-WebAppPool -Name "MyAppPool"
Stop-WebAppPool -Name "MyAppPool"
Restart-WebAppPool -Name "MyAppPool"

# Find stopped pools (often a symptom of rapid-fail protection tripping)
Get-ChildItem IIS:\AppPools | Where-Object { $_.State -eq "Stopped" }

# Watch a worker process's private memory in real time
Get-Counter '\Process(w3wp*)\Private Bytes'

A pool sitting in the Stopped state that you didn't stop manually is almost always rapid-fail protection having tripped — check the System and Application event logs for the underlying crash before just restarting it, or you'll be back at the same 503 five crashes later.

Key Takeaways

  • Use ApplicationPoolIdentity unless a specific downstream resource requires a named domain service account
  • Scheduled and memory-based recycling are safety nets, not fixes — a recycle that "resolves" growing memory usage is hiding a leak, not fixing one
  • Pair startMode=AlwaysRunning with Application Initialization preload to eliminate cold-start latency after idle shutdown or recycle
  • Leave new applications and all ASP.NET Core apps on Integrated pipeline mode; Classic exists only for legacy ISAPI-era compatibility
  • Leave overlapping recycling enabled (the default) so the old worker process keeps serving in-flight requests while the new one spins up

Related Articles

IIS Installation & ConfigurationIIS SSL/TLS Configuration: HTTPS and Certificate ManagementIIS Logging, Performance & Troubleshooting Common ErrorsIIS URL Rewrite and ARR: Reverse Proxy Setup