Preventing Application “Recycles” (.NET Core / Blazor) Print

  • recycle, app pool recycle, recycle pool, app slow, application failed to load
  • 0

Building Recycle-Resilient .NET Core & Blazor Applications on IIS

Summary: Application pool recycling is a normal, required part of hosting. A well-designed .NET Core or Blazor application should survive a recycle with zero user impact. This guide explains why recycles happen, what you can control, and the specific patterns to implement so your users never notice them.


Understanding Recycling on Shared Hosting

On Adaptive Web Hosting's shared IIS environment, every site runs in an isolated application pool. Recycling is intentionally kept enabled because it protects platform stability, enforces fair resource allocation, and recovers from leaks or crashes automatically. Disabling recycling is not an option we offer on shared plans — but in our experience, the vast majority of "my app keeps restarting" tickets are resolved at the application layer, not the platform layer.

What triggers a recycle

  • Memory limits being reached (defined per hosting plan)
  • CPU throttling thresholds
  • Unhandled exceptions or process crashes
  • File changes from a deployment
  • Scheduled platform maintenance or .NET runtime updates

Recycle vs. idle shutdown — they're not the same

If your app pool has no traffic for a period of time, IIS may shut the worker process down entirely until the next request arrives. This is an idle shutdown, not a recycle, and the symptoms (slow first request, lost in-memory state) look identical. The fix is different though — it requires startMode="AlwaysRunning" on the app pool plus the Application Initialization module, both of which need to be enabled by support on shared plans. Open a ticket if cold starts are your primary issue.


The Core Principle

The goal is not to prevent recycling. The goal is to make your application stateless, resilient, and restart-safe so that a recycle is invisible to users. Every recommendation below serves that principle.


What Not to Do

  • Do not rely on in-memory session state in production
  • Do not store user data, carts, or auth context in static fields or unbacked singletons
  • Do not assume your worker process will run for hours or days uninterrupted
  • Do not use the in-process default for data protection keys (more on this below — it's the #1 cause of "users get logged out randomly" tickets)

Required Configuration for Production Apps

1. Persist Data Protection Keys

This is the single most important item on this list, and the one most often missed. ASP.NET Core uses Data Protection keys to encrypt authentication cookies, antiforgery tokens, and TempData. By default these keys live in memory or in a per-process folder — meaning every recycle generates a new key, and every existing auth cookie becomes invalid. Your users get silently logged out.

Persist the keys to a stable location:

// Option A: File system path that survives recycles
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"C:\inetpub\keys\myapp"))
    .SetApplicationName("MyApp");

// Option B: Redis (preferred if you already use Redis)
builder.Services.AddDataProtection()
    .PersistKeysToStackExchangeRedis(redisConnection, "DataProtection-Keys")
    .SetApplicationName("MyApp");

If you're on AWH shared hosting and need a persistent path outside your web root, contact support — we'll provision one for you.

2. Use Real Distributed Session Storage

If your app uses sessions, move them out of process memory. Important: AddDistributedMemoryCache() is misleadingly named — it is not distributed. It's an in-process implementation of the IDistributedCache interface, and it offers zero protection against recycles. Use it only for local development.

For production, use Redis or SQL Server:

// Production: Redis-backed session
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "MyApp:";
});

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

SQL Server-backed session is also supported via AddDistributedSqlServerCache() if you prefer to avoid an additional service.

3. Persist Application State Externally

Anything that needs to survive a recycle must live outside the worker process. That includes:

  • User accounts, profiles, preferences — database
  • Shopping carts, draft documents, in-progress workflows — database or distributed cache with TTL
  • Reference data, lookup tables — distributed cache or database with local cache layer
  • Background job state — durable queue (Hangfire with SQL backing, Azure Service Bus, etc.)

The anti-pattern to avoid:

// DON'T: lost on every recycle
private static List<UserData> _users = new();
private static Dictionary<Guid, ShoppingCart> _carts = new();

4. Handle Graceful Shutdown

When IIS recycles, your app receives a shutdown signal with a brief grace period to clean up. Use it to flush logs, complete in-flight work, and close resources cleanly.

app.Lifetime.ApplicationStopping.Register(() =>
{
    // Flush logs, save state, close connections
    logger.LogInformation("Application stopping - cleaning up");
});

app.Lifetime.ApplicationStopped.Register(() => {     // Final cleanup after shutdown completes
});

5. Add Retry Logic for External Calls

Recycles can interrupt database queries, HTTP calls to upstream APIs, and message bus operations. Polly is the standard library for resilience policies in .NET:

builder.Services.AddHttpClient("default")
    .AddTransientHttpErrorPolicy(policy =>
        policy.WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))
        ));

For Entity Framework Core, enable retry-on-failure in your DbContext configuration:

options.UseSqlServer(connectionString, sql => sql.EnableRetryOnFailure(
    maxRetryCount: 5,
    maxRetryDelay: TimeSpan.FromSeconds(10),
    errorNumbersToAdd: null));

6. Implement Health Checks

Health checks let monitoring tools verify your app has fully recovered after a restart, and they make it easy for support to diagnose issues quickly.

builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString)
    .AddRedis(redisConnection)
    .AddCheck<CustomDependencyCheck>("custom-dependency");

app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

7. Configure Persistent Logging

Many reported "recycles" are actually unhandled exceptions causing the worker to crash. Without persistent logs you cannot tell the difference. Use file-based logging at minimum, ideally with a structured logger like Serilog:

builder.Host.UseSerilog((context, config) => config
    .WriteTo.Console()
    .WriteTo.File(
        path: "logs/app-.log",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 14)
    .Enrich.FromLogContext());

8. Avoid Long-Running In-Memory Work

Background loops and in-memory job queues running inside your web app will lose work on every recycle. Move them to a durable queue with a worker process:

  • Hangfire with SQL Server storage — easy to set up, jobs survive recycles
  • Quartz.NET with persistent job store
  • External worker process running as a Windows Service for heavy or long-running work

If you must use IHostedService, ensure every operation is idempotent and checkpoints progress to durable storage.

9. Manage Memory Carefully

Hitting the memory ceiling on your hosting plan triggers an immediate recycle. Common offenders:

  • Unbounded IMemoryCache — always set SizeLimit and provide entry sizes
  • Loading entire datasets into memory instead of streaming
  • Event handler leaks — subscribers not unsubscribing
  • Large object heap fragmentation from oversized allocations
  • Improperly disposed HttpClient, database connections, or file handles

Use dotnet-counters or Application Insights to profile memory before going to production.


Special Notes for Blazor

Blazor Server

Blazor Server is the most recycle-sensitive .NET workload because circuits live in server memory. When the worker recycles, every active circuit dies and users see a "Reconnecting..." overlay. Mitigation:

  • Persist any meaningful UI state to the database or browser storage, not just the circuit
  • Implement a custom CircuitHandler to detect disconnects and warn users before they lose unsaved work
  • Tune SignalR reconnection — the defaults are reasonable but can be extended for spotty networks
  • Persist data protection keys (see item 1) — without this, the auth cookie is invalidated on recycle and reconnection fails
builder.Services.AddServerSideBlazor()
    .AddCircuitOptions(options =>
    {
        options.DetailedErrors = builder.Environment.IsDevelopment();
        options.DisconnectedCircuitMaxRetained = 100;
        options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3);
    });

Blazor WebAssembly

The UI itself is unaffected by server recycles since it runs in the browser. However, any API call made during a recycle window will fail — so retry logic on your HttpClient (item 5 above) is mandatory, not optional. A single failed API call with no retry will surface to the user as a broken page.


Cold Start Mitigation

If your app feels slow on the first request after a quiet period, you're hitting a cold start. Two things help:

1. Application pool always-running mode. Requires support assistance on shared hosting — open a ticket and we'll enable startMode="AlwaysRunning" on your app pool along with the Application Initialization module.

2. A warmup endpoint. Once always-running is enabled, IIS can hit a warmup URL automatically after each recycle:

<applicationInitialization doAppInitAfterRestart="true">
    <add initializationPage="/warmup" />
</applicationInitialization>

Your /warmup endpoint should pre-load caches, JIT-compile hot paths, and verify dependencies are reachable.


When to Contact AWH Support

The application-level fixes above resolve most issues. Open a ticket if:

  • Your app pool is recycling more than a few times per hour and you've ruled out memory leaks and unhandled exceptions in your logs
  • You need a persistent file path provisioned for data protection keys or session state
  • You need startMode="AlwaysRunning" and the Application Initialization module enabled on your app pool
  • You're seeing recycles correlated with platform events (your other sites on the server are experiencing issues at the same time)
  • Your hosting plan's memory limit appears too low for your workload — we can advise on plan sizing or dedicated options

When opening a ticket, include: timestamps of recent recycles, relevant log excerpts, your app URL, and a short description of what you've already tried. This dramatically speeds up diagnosis.


Bottom Line

A properly designed .NET Core or Blazor application should survive an IIS recycle with zero user impact. The patterns in this guide — persistent data protection keys, real distributed session storage, externalized state, retry policies, health checks, and durable background work — are the industry standard for resilient web applications, not just workarounds for shared hosting. Apply them and your app will be more robust everywhere it runs.


Was this answer helpful?

« Back