Skip to content

IIS & Windows Service Hosting

Key Points

  • In-process IIS hostingIIS loads CLR via the ASP.NET Core Module v2 (ANCMv2). Fastest; default since .NET Core 2.2.
  • Out-of-process IIS hostingIIS reverse-proxies to a Kestrel child process over HTTP. Slower but useful for sandbox/legacy isolation.
  • Windows Service hostingUseWindowsService() runs Kestrel as a Windows Service (no IIS), tied to the SCM lifecycle. UseSystemd() is the Linux equivalent.
  • web.config is still relevant for IIS (ANCM settings, app pool environment); not for non-IIS hosting.
  • 2026 reality: Linux containers + Kestrel + a reverse proxy (nginx/AGW) has overtaken IIS for new services. IIS persists in Windows-bound enterprises and where existing operational tooling assumes it.

Concepts (deep dive)

Hosting models

┌─── In-process (default) ────────────┐    ┌─── Out-of-process ──────┐
│                                      │    │                         │
│   IIS Worker (w3wp.exe)              │    │   IIS Worker            │
│   └─ ANCM v2 (managed)               │    │   └─ ANCM v2            │
│      └─ Loads CLR                    │    │      └─ Spawns dotnet.exe│
│         └─ App + IIS HttpServer      │    │         └─ Kestrel       │
│                                      │    │                         │
│   No HTTP hop                        │    │   IIS ↔ Kestrel via HTTP│
│   No Kestrel                         │    │   Loopback localhost    │
└──────────────────────────────────────┘    └─────────────────────────┘

In-process specifics

  • App pool process is the .NET process — single container.
  • Program.cs calls WebApplication.CreateBuilder(args) — auto-detects in-process via env var.
  • IISServerOptions configures the IIS HTTP server inside the worker.
  • Faster: ~5-10× lower request latency vs out-of-process.
  • Limitation: one app pool per worker, can't crash-isolate multiple apps.

Out-of-process specifics

<!-- web.config -->
<system.webServer>
  <handlers>
    <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" />
  </handlers>
  <aspNetCore processPath="dotnet" arguments=".\MyApp.dll"
              hostingModel="OutOfProcess" stdoutLogEnabled="false" />
</system.webServer>

ANCM spawns dotnet MyApp.dll, manages restart on crash, proxies HTTP. Useful for: - Multiple apps per pool with isolation. - Apps with side-effects on global state that misbehave inside IIS. - Sandbox environments.

Forwarded headers and IIS

ANCM forwards client info to Kestrel automatically — UseIISIntegration() (auto-called by CreateBuilder) wires it up. You generally do not need UseForwardedHeaders for IIS-only setups. For multi-hop (IIS behind another proxy), do.

web.config cheatsheet

<configuration>
  <system.webServer>
    <handlers>
      <remove name="aspNetCore" />
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%"
                hostingModel="inprocess" stdoutLogEnabled="true"
                stdoutLogFile=".\logs\stdout">
      <environmentVariables>
        <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
        <environmentVariable name="MY_FEATURE_FLAG" value="true" />
      </environmentVariables>
    </aspNetCore>
  </system.webServer>
</configuration>

stdoutLogEnabled="true" for crash diagnostics during deployment. Disable in steady state.

Windows Service hosting

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Host.UseWindowsService();   // ← key line
        // ...
        var app = builder.Build();
        app.MapGet("/", () => "Hello");
        app.Run();
    }
}
sc create MyApi binPath= "C:\apps\MyApi\MyApi.exe" start= auto
sc start MyApi

UseWindowsService adapts the host to: - Lifecycle events (Start/Stop/Pause via SCM). - Logging to Event Viewer. - ContentRoot defaults to the EXE directory.

For background workers (no HTTP), use Host.CreateApplicationBuilder(args) + UseWindowsService + IHostedService.

UseSystemd() (Linux equivalent)

builder.Host.UseSystemd();
# /etc/systemd/system/myapi.service
[Service]
Type=notify
ExecStart=/usr/bin/dotnet /opt/myapi/MyApi.dll
Restart=always
User=www-data
[Install]
WantedBy=multi-user.target

Type=notify lets the host signal "ready" to systemd — better than simple for graceful starts.

App pool settings worth tuning

Idle Time-out (minutes): 0 (disable; startup cost is real for ASP.NET Core)
Recycling: time-based off; memory-based on (~1.5 GB)
Start Mode: AlwaysRunning
Preload Enabled: True

AlwaysRunning + Preload means IIS warms up the app at OS start, not on first request — avoids cold-start penalty.

Hot-deploy on IIS

In-process: copying a new DLL triggers a recycle when ANCM detects the file change. Use app_offline.htm for clean deploys:

cp app_offline.htm c:\inetpub\myapi\
sleep 5
# now copy new bits
rm c:\inetpub\myapi\app_offline.htm

While app_offline.htm exists, IIS serves it and the app process is shut down — atomic-ish swap.

When to choose what

Hosting Use when
Linux containers + Kestrel + nginx New services, cross-platform, cloud-native
IIS in-process Windows enterprise, existing IIS ops, mixed .NET Framework + Core
IIS out-of-process Need sandbox/isolation per app, legacy interop
Windows Service On-prem Windows servers, no IIS
Linux systemd On-prem Linux, no container orchestrator

Code: correct vs wrong

❌ Wrong: assuming Kestrel binds in IIS in-process

builder.WebHost.UseUrls("http://localhost:5000");   // ignored under IIS in-process

In-process uses IIS HTTP server, not Kestrel. URL bindings come from IIS site config.

✅ Correct: bind via IIS site

Configure URLs in IIS Manager / applicationhost.config. The app accepts whatever IIS routes.

❌ Wrong: missing graceful shutdown for Windows Service

public static void Main(string[] args) { /* no UseWindowsService */ Host.CreateDefaultBuilder().Build().Run(); }

SCM stop → process killed before requests drain.

✅ Correct

builder.Host.UseWindowsService();
// + use IHostApplicationLifetime to drain in-flight work on stop

❌ Wrong: stdoutLogEnabled=true in production

Logs accumulate in \logs\stdout_*.log indefinitely; disk fills.

✅ Correct

stdoutLogEnabled="false" for steady state; flip to true when investigating startup crashes.


Design patterns for this topic

Pattern 1 — app_offline.htm deploys

PowerShell wrapper drops app_offline.htm, waits, swaps bits, removes file. Predictable, atomic-ish.

Pattern 2 — Windows Service worker for background jobs

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<EmailDispatcher>();
builder.Host.UseWindowsService();
builder.Build().Run();

For non-HTTP workers on Windows servers without IIS.

Pattern 3 — Health-checked SCM lifecycle

app.Lifetime.ApplicationStopping.Register(() =>
{
    _logger.LogInformation("Shutdown signal received; draining...");
    // Stop accepting new work; let in-flight finish
});

SCM stop fires Stopping; you have ~30 s by default to drain.

Pattern 4 — Mixed-runtime: IIS hosts both Framework and Core apps

App pools per app, in-process for Core, classic pipeline for Framework. Single IIS, gradual migration.


Pros & cons / trade-offs

Hosting Pros Cons
IIS in-process Fastest, integrates with Windows ops Windows-only, IIS dependencies
IIS out-of-process Sandbox, restart on crash Slower, double process
Kestrel + reverse proxy Cross-platform, container-native More moving parts
Windows Service No IIS required Manual install, Windows-only
systemd Standard on Linux Linux-only

When to use / when to avoid

  • New cloud services: containers + reverse proxy, not IIS.
  • Existing Windows enterprise: IIS in-process for parity with .NET Framework apps.
  • On-prem long-running workers: Windows Service / systemd.
  • Avoid out-of-process IIS unless you need its specific isolation.

Interview Q&A

Q1. Difference between in-process and out-of-process IIS hosting? A. In-process: ANCM loads the CLR into w3wp; the app runs in the IIS worker. Out-of-process: ANCM spawns dotnet.exe and IIS proxies HTTP to Kestrel via loopback. In-process is faster; out-of-process gives isolation.

Q2. What is ANCMv2? A. ASP.NET Core Module v2 — IIS native module that hosts ASP.NET Core apps. Required on the IIS server (ships with the .NET Core Hosting Bundle).

Q3. Do you need UseForwardedHeaders under IIS? A. Not for the IIS hop — UseIISIntegration handles it. Yes if there's another proxy in front of IIS.

Q4. What does UseWindowsService() do? A. Adapts the generic host: integrates with SCM lifecycle, logs to Event Viewer, sets ContentRoot to the EXE folder. Required when running as a Windows Service.

Q5. Why is Type=notify better than Type=simple in systemd? A. The host signals readiness to systemd when bootstrap is complete. simple declares ready as soon as the process starts, before the app can serve traffic. notify lets dependent services wait correctly.

Q6. What's app_offline.htm? A. A magic IIS file: when present, IIS shuts down the app pool and serves app_offline.htm for all requests until removed. Used for atomic deploys.

Q7. What does Always Running start mode do? A. IIS launches the app pool at server start instead of on first request. Removes cold-start latency at cost of always-on memory.

Q8. Why might in-process hosting break a feature that worked out-of-process? A. In-process shares the IIS thread/handle ecosystem; some libraries that assume Kestrel-style HTTP server (e.g., raw HttpListener quirks, custom WebSocket plumbing) misbehave. Test under in-process explicitly.


Gotchas / common mistakes

  • UseUrls in code — ignored under IIS in-process.
  • stdoutLogEnabled=true left on — fills disk over months.
  • IIS app pool identity lacks file-system rights — app can't read config / write logs.
  • Forgetting to install the .NET Hosting Bundle on the IIS server — 500.31/500.30 errors.
  • Hot-deploying without app_offline.htm — half-served requests during file copy.
  • Windows Service without UseWindowsService — SCM stop kills mid-request.
  • systemd Type=simple for ASP.NET Core — dependent services start too early.
  • Confusing applicationhost.config (IIS Express) with applicationHost.config (IIS) — they're separate files for different hosts.

Further reading