IIS & Windows Service Hosting
Key Points
- In-process IIS hosting — IIS loads CLR via the ASP.NET Core Module v2 (ANCMv2). Fastest; default since .NET Core 2.2.
- Out-of-process IIS hosting — IIS reverse-proxies to a Kestrel child process over HTTP. Slower but useful for sandbox/legacy isolation.
- Windows Service hosting —
UseWindowsService()runs Kestrel as a Windows Service (no IIS), tied to the SCM lifecycle.UseSystemd()is the Linux equivalent. web.configis 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.cscallsWebApplication.CreateBuilder(args)— auto-detects in-process via env var.IISServerOptionsconfigures 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();
}
}
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)
# /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
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
❌ 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
UseUrlsin code — ignored under IIS in-process.stdoutLogEnabled=trueleft 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=simplefor ASP.NET Core — dependent services start too early. - Confusing
applicationhost.config(IIS Express) withapplicationHost.config(IIS) — they're separate files for different hosts.