On IIS, an ASP.NET Core 10 app is only as warm as its worker process. After an app pool recycle—idle timeout, scheduled recycle, config change, or deploy—the first real request still pays for runtime startup, DI graph build, config bind, and whatever your Program.cs touches before Kestrel (via ANCM) is ready. That delay shows up as a multi-second blip for users and monitors even when the network SLA is fine.

IIS Application Initialization exists to own that first hit. Paired with a cheap warmup endpoint in your .NET 10 app, it turns “cold after recycle” into a controlled preload instead of a surprise for production traffic. This is hosting-floor work: web.config, app pool flags, and a few lines of C#—not a rewrite of your architecture.

#What actually goes cold on recycle

In-process hosting (the default for modern ASP.NET Core on IIS) loads your app inside w3wp.exe. Recycle tears that process down. Framework-dependent .NET 10 deployments still need the shared runtime on the machine, but they do not skip managed startup: host builder, middleware pipeline, EF Core context pooling setup, static option binding, and any IHostedService that starts work in StartAsync. Out-of-process is similar from the client’s point of view—ANCM waits on the child until it listens. Either way, the first external request is a poor choice of warmup trigger.

#Enable Application Initialization for the site

Install the Application Initialization role service on Windows Server if it is not already present (IIS 10.0 on Windows Server 2025 still uses this feature set). Then tell IIS which path to hit when the worker starts, and that the app should initialize without waiting for a browser.

xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <applicationInitialization
        doAppInitAfterRestart="true"
        skipManagedModules="false">
      <add initializationPage="/health/warmup" hostName="localhost" />
    </applicationInitialization>
    <handlers>
      <add name="aspNetCore" path="*" verb="*"
           modules="AspNetCoreModuleV2"
           resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="dotnet"
                arguments=".\MyApp.dll"
                stdoutLogEnabled="false"
                hostingModel="inprocess" />
  </system.webServer>
</configuration>

doAppInitAfterRestart="true" is the switch that matters after pool recycles and Web Deploy publishes that bounce the app. hostName="localhost" keeps the init request on the box; point initializationPage at a route that does not require auth, antiforgery, or external dependencies that might be down during boot. On shared hosts you typically cannot install roles yourself—confirm Application Initialization is available and that your web.config section is unlocked before relying on it.

#Add a deliberate warmup path in .NET 10

Do not reuse your public /healthz liveness probe if that probe is intentionally shallow. Warmup should exercise the expensive, once-per-process work you care about: build a service scope, open a SQL connection, hit memory cache, JIT a hot path. Keep it idempotent and fast enough that IIS init does not look hung.

csharp
// Program.cs — ASP.NET Core 10 / .NET 10
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<WarmupState>();
// ... AddControllers, AddDbContext, etc.

var app = builder.Build();

app.MapGet("/health/warmup", async (WarmupState state, IConfiguration config) =>
{
    if (state.Completed)
        return Results.Ok(new { status = "already-warm" });

    // Touch config + a real dependency once per process
    var cs = config.GetConnectionString("Default");
    await using var conn = new Microsoft.Data.SqlClient.SqlConnection(cs);
    await conn.OpenAsync();
    await using var cmd = conn.CreateCommand();
    cmd.CommandText = "SELECT 1";
    _ = await cmd.ExecuteScalarAsync();

    state.Completed = true;
    return Results.Ok(new { status = "warmed" });
});

app.MapGet("/healthz", () => Results.Ok(new { status = "live" }));

app.Run();

sealed class WarmupState
{
    public bool Completed { get; set; }
}

Map the warmup route before auth middleware if your pipeline is global. Prefer Microsoft.Data.SqlClient against SQL Server with the same connection string the app uses in production so pool and TLS settings match. If you use EF Core 10, a single lightweight DbContext query is fine—avoid Migrate() on this path.

#App pool settings that decide whether init helps

  • Start Mode = AlwaysRunning on the app pool so Windows starts w3wp without waiting for traffic (where you control the pool).
  • Preload Enabled = true on the site/application so IIS issues the initialization request when the worker starts.
  • Idle Timeout = 0 (or a long value) if idle kills are causing unnecessary cold starts on low-traffic apps.
  • Regular time interval recycles: schedule them off-peak; init still runs, but you avoid overlapping with deploys.
  • ANCM hostingModel=inprocess in web.config for lower overhead; keep the ASP.NET Core Hosting Bundle aligned with your .NET 10 runtime on the server.

On shared Windows hosting you may only control web.config and publish output—not AlwaysRunning. In that case Application Initialization still helps whenever the pool does start, and a shallow /healthz plus synthetic monitor reduces how often real users take the first hit. On a Windows VPS you should set pool and site preload explicitly.

#Quick verification after publish

After Web Deploy (or a copy to the site root), recycle the pool once and watch both IIS logs and your app logs. You want a request to /health/warmup from the local init path before customer traffic. A simple local check:

powershell
# Run on the server after a controlled recycle
Invoke-WebRequest -Uri "http://localhost/health/warmup" -UseBasicParsing
Invoke-WebRequest -Uri "http://localhost/healthz" -UseBasicParsing
# Confirm w3wp is running and listening under the site’s app pool identity

If warmup returns 500, fix that before enabling doAppInitAfterRestart in production—failed init is worse than a lazy cold start. Log the warmup path at Information so you can correlate with recycle events. Skip stuffing heavy report generation or remote third-party calls into init; those belong behind queues, not the IIS preload request.

Practical takeaway: for ASP.NET Core 10 on IIS, treat recycle as a planned event. Ship a dedicated /health/warmup route that opens SQL and marks process-level readiness, enable applicationInitialization with doAppInitAfterRestart in web.config, and turn on site preload plus AlwaysRunning wherever you own the app pool. That combination removes most first-request stalls without changing your hosting model or abandoning framework-dependent publishes that fit Windows shared and VPS layouts.