Preventing IIS Application Pool Recycling for WCF Service Performance Optimization


2 views

When dealing with WCF services in IIS, application pool recycling can significantly impact performance, especially when your service performs expensive initialization tasks. Let's break down the key concepts:

// Example of expensive initialization in Global.asax
protected void Application_Start(object sender, EventArgs e)
{
    // This heavy operation gets wiped during recycling
    InitializeGlobalCache(); // Takes 2+ minutes
    WarmUpServiceEndpoints(); // CPU-intensive
}

You've correctly identified three critical settings that affect recycling:

  • CPU Limit Interval (0 = disabled): When set to non-zero, IIS monitors CPU usage over this interval (minutes). If the limit is exceeded, recycling occurs.
  • Idle Time-out (0 = disabled): Worker process shuts down after specified idle time. Different from recycling as it's triggered by inactivity.
  • Regular Time Interval (0 = disabled): Forces recycling at fixed intervals regardless of system health.

The distinction between worker process shutdown and application pool recycling:

Worker Process Shutdown Application Pool Recycling
Triggered by idle timeout Triggered by schedule/config changes
Complete process termination New process spins up before old terminates
All sessions lost immediately Overlapping processes during switch

Beyond the settings you've modified, consider these configurations:

<applicationPools>
    <add name="MyWcfAppPool" 
         managedRuntimeVersion="v4.0"
         startMode="AlwaysRunning">
        <recycling logEventOnRecycle="Time,Requests">
            <periodicRestart time="00:00:00"/>
        </recycling>
        <processModel idleTimeout="00:00:00"/>
    </add>
</applicationPools>

For services that absolutely cannot tolerate recycling:

  1. Implement a state persistence layer that survives recycling
  2. Use Windows Service hosting as a fallback
  3. Create a "warmup" endpoint that triggers initialization
// Example warmup endpoint
[OperationContract]
public void PreloadCache()
{
    if (CacheManager.IsInitialized) return;
    
    lock(_initLock)
    {
        InitializeGlobalCache();
    }
}

Remember that completely disabling recycling isn't recommended for production environments. Instead, implement proper cache serialization and initialization strategies to minimize the impact when recycling does occur.


When dealing with WCF services that perform costly initialization (like loading large datasets into memory), IIS recycling becomes a critical pain point. The application pool recycling behavior you're experiencing is actually IIS's default health management mechanism.

The modifications you implemented address the three primary triggers for recycling:

  • CPU Limit (0): Prevents recycling when CPU usage exceeds a threshold over the monitoring period
  • Idle Time-out (0): Stops IIS from shutting down inactive worker processes
  • Regular Time Interval (0): Disables scheduled recycling

1. CPU Limit Interval Explained

The CPU Limit setting has two components:

Limit        - Maximum percentage CPU usage allowed (0 = no limit)
Limit Interval - Monitoring window in minutes (0 = disabled)

Setting both to 0 completely disables CPU-based recycling.

2. What "Recycled" Actually Means

Recycling initiates a graceful shutdown sequence:

  1. IIS stops routing new requests to the worker process
  2. Existing requests complete or time out
  3. The w3wp.exe process terminates
  4. A new worker process starts with fresh initialization

3. Worker Process vs Application Pool Recycling

The key distinction:

Worker Process Shutdown Application Pool Recycling
Affects single process instance Full restart of the app pool environment
Triggered by inactivity (Idle Time-out) Triggered by schedule/config changes
Memory is completely released May preserve some state (depending on config)

For mission-critical WCF services, consider these extra safeguards:

Windows Service Hosting Alternative

Create a hybrid hosting solution:

// In Program.cs
public static void Main()
{
    // Run as Windows Service if not interactive
    if (!Environment.UserInteractive)
    {
        ServiceBase.Run(new WcfWindowsService());
        return;
    }
    
    // Otherwise run as console app for debugging
    StartWcfHost();
    Console.ReadLine();
    StopWcfHost();
}

Persistent Cache Storage

Implement a disk-based cache fallback:

public class ExpensiveResourceCache
{
    private static readonly string CachePath = @"C:\wcf_cache\resource.dat";
    
    public static Resource GetResource()
    {
        if (File.Exists(CachePath) && 
            File.GetLastWriteTime(CachePath) > DateTime.Now.AddHours(-1))
        {
            return Deserialize(File.ReadAllBytes(CachePath));
        }
        
        var resource = FetchResourceFromSource();
        File.WriteAllBytes(CachePath, Serialize(resource));
        return resource;
    }
}

Application Initialization Module

Configure in applicationHost.config:

<applicationPools>
    <add name="MyAppPool" startMode="AlwaysRunning">
        <recycling logEventOnRecycle="Time, Requests, Schedule" />
    </add>
</applicationPools>

<sites>
    <site name="MySite">
        <application path="/" preloadEnabled="true" />
    </site>
</sites>

Add this diagnostic code to detect recycling events:

protected void Application_Start()
{
    var currentPID = Process.GetCurrentProcess().Id;
    Application["InstanceID"] = currentPID;
    
    AppDomain.CurrentDomain.DomainUnload += (s, e) => 
    {
        Logger.Warn($"AppDomain unloading! PID: {currentPID}");
    };
}

protected void Application_BeginRequest()
{
    var runningPID = (int?)Application["InstanceID"];
    if (runningPID != Process.GetCurrentProcess().Id)
    {
        Logger.Error($"PROCESS RECYCLED! Was {runningPID}, now {Process.GetCurrentProcess().Id}");
    }
}

Verify these settings in applicationHost.config:

<applicationPools>
    <add name="MyAppPool"
         autoStart="true"
         startMode="AlwaysRunning"
         queueLength="5000"
         managedRuntimeVersion="v4.0">
        <recycling disallowOverlappingRotation="false"
                  disallowRotationOnConfigChange="true"
                  logEventOnRecycle="Time, Memory, PrivateMemory">
            <schedule>
                <clear />
            </schedule>
        </recycling>
        <cpu limit="0" action="NoAction" resetInterval="0" />
        <processModel idleTimeout="0" 
                      maxProcesses="1" 
                      shutdownTimeLimit="90" 
                      startupTimeLimit="90" />
    </add>
</applicationPools>