Hosting & Kestrel
Key Points
- The .NET Generic Host (
Microsoft.Extensions.Hosting) is the unified host for web, console, and worker apps.WebApplication.CreateBuilder()builds on top of it. - Kestrel is the cross-platform managed web server. It's the default and recommended server for ASP.NET Core in 2026, including production behind an edge proxy or directly Internet-facing (with HTTPS + connection limits).
IHost.RunAsync()blocks until shutdown. Graceful shutdown is signaled byIHostApplicationLifetime.ApplicationStopping— wired automatically toSIGTERM/Ctrl+C.- HTTP/1.1, HTTP/2, HTTP/3 are all supported. HTTP/3 (over QUIC) is opt-in; HTTP/2 is default for HTTPS endpoints.
UseUrlsvsListen/ListenAnyIp— the latter give per-endpoint control (transport, certificate, HTTP version).ConfigureKestrelis where you set limits:Limits.MaxConcurrentConnections,KeepAliveTimeout,RequestHeadersTimeout,MaxRequestBodySize, etc.- Reverse proxies (NGINX, IIS, YARP, Azure Front Door) sit in front in many deployments — but Kestrel can serve directly.
Concepts (deep dive)
The host — what it does
var builder = WebApplication.CreateBuilder(args);
// builder.Services, builder.Configuration, builder.Logging available
builder.Services.AddSingleton<IFoo, Foo>();
var app = builder.Build();
// app is the IHost. Routes, middleware, etc. configured below.
app.MapGet("/", () => "Hello");
await app.RunAsync();
// Returns when the host stops.
The host is responsible for:
- Configuration — loads
appsettings.json, environment variables, command-line args, secrets. - Logging — bootstraps
ILogger<T>from configured providers. - DI container — builds the service provider; registers
IHostedServices. - Hosted services lifecycle — calls
StartAsyncon each in registration order; callsStopAsyncin reverse on shutdown. - Application lifetime events —
ApplicationStarted,ApplicationStopping,ApplicationStopped(IHostApplicationLifetime).
CreateBuilder
│
▼
Configuration sources loaded
│
▼
Services registered
│
▼
Build → IHost
│
▼
RunAsync()
├─ start hosted services (in order)
├─ open Kestrel listeners
├─ accept requests
▼
Shutdown signal (SIGTERM / Ctrl+C)
├─ ApplicationStopping fires
├─ stop hosted services (reverse order)
├─ ApplicationStopped fires
└─ exit
Graceful shutdown
public class Worker(IHostApplicationLifetime lifetime) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stopping)
{
// stopping is signaled when SIGTERM arrives; default 30s before forced kill.
try
{
while (!stopping.IsCancellationRequested)
{
await DoWorkAsync(stopping);
}
}
catch (OperationCanceledException) { /* normal */ }
}
}
The host's shutdown timeout defaults to 30 seconds. Configure via HostOptions:
builder.Services.Configure<HostOptions>(o =>
{
o.ShutdownTimeout = TimeSpan.FromSeconds(60);
o.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.StopHost;
});
Kubernetes sends SIGTERM then waits terminationGracePeriodSeconds (default 30s) before SIGKILL. Match the host's ShutdownTimeout to leave headroom.
Kestrel configuration
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 1000;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50 MB
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.MinRequestBodyDataRate = new MinDataRate(
bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Any, 5000);
options.Listen(IPAddress.Any, 5001, lo =>
{
lo.UseHttps("cert.pfx", "password");
lo.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});
});
Or via configuration:
// appsettings.json
{
"Kestrel": {
"Endpoints": {
"Http": { "Url": "http://*:5000" },
"Https": { "Url": "https://*:5001", "Protocols": "Http1AndHttp2AndHttp3" }
},
"Limits": {
"MaxConcurrentConnections": 1000,
"MaxRequestBodySize": 52428800
}
}
}
💡 Senior tip:
MaxConcurrentConnectionsdefaults to no limit — under SYN flood you can OOM. Always set a cap.
HTTPS
options.Listen(IPAddress.Any, 5001, lo =>
{
lo.UseHttps(httpsOptions =>
{
httpsOptions.ServerCertificate = LoadCert();
httpsOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
httpsOptions.ClientCertificateMode = ClientCertificateMode.NoCertificate;
});
});
In production, certificates typically come from Azure Key Vault, Let's Encrypt automation (certbot, lego), or Kubernetes cert-manager. For local dev, dotnet dev-certs https --trust installs a self-signed cert.
IHostedService lifecycle
public class WarmupService(ICache cache) : IHostedService
{
public Task StartAsync(CancellationToken ct)
{
cache.Warm(); return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
builder.Services.AddHostedService<WarmupService>();
Hosted services start in registration order, stop in reverse. StartAsync blocks the host's startup — keep it fast, or use BackgroundService for long-running work.
⚠️
StartAsyncblocks startup. If it takes 30s, the listener doesn't bind for 30s. Either keep it fast or move work intoBackgroundService.ExecuteAsync(which runs in the background afterStartreturns).
BackgroundService for long-running work
public class Worker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stopping)
{
while (!stopping.IsCancellationRequested)
{
await DoWorkAsync(stopping);
await Task.Delay(TimeSpan.FromMinutes(1), stopping);
}
}
}
BackgroundService.ExecuteAsync returns a Task representing the lifetime of the service. The host monitors it: if it throws or completes early, depending on BackgroundServiceExceptionBehavior, the host either keeps running or shuts down.
See Hosted Services & Background Work for the full treatment.
Reverse proxy or direct?
| Topology | Pros | Cons |
|---|---|---|
| Kestrel direct (Internet) | One process; simpler ops | Need to harden TLS / DDoS / WAF yourself |
| Behind NGINX / Azure Front Door | Edge concerns offloaded; centralized SSL | One more hop |
| Behind YARP | First-party .NET reverse proxy; integrates well | Same .NET process or a dedicated YARP instance |
| Behind IIS (Windows) | Windows-native deployment | Tied to Windows |
In containers, Kestrel direct is most common (the Kubernetes Ingress acts as the edge proxy). On Azure App Service, App Service is the edge.
UseForwardedHeaders for proxied scenarios
When behind a proxy, HttpContext.Connection.RemoteIpAddress will be the proxy's IP. To see the real client:
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
options.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
});
var app = builder.Build();
app.UseForwardedHeaders(); // BEFORE other middleware
Configure KnownNetworks / KnownProxies — otherwise headers can be spoofed by clients.
Slim host — minimal footprint
var builder = WebApplication.CreateSlimBuilder(args);
// Smaller default service registrations; AOT-friendly
CreateSlimBuilder (or CreateEmptyBuilder) trims default services, useful for NativeAOT or microservices that don't need full ASP.NET Core defaults.
How it works under the hood
WebApplication.CreateBuilder is a thin wrapper over Host.CreateApplicationBuilder plus ASP.NET Core defaults: Kestrel server, routing, default logging providers, etc.
Kestrel uses SocketsHttpHandler-style transport for connection acceptance, with HTTP/1.1, HTTP/2 (multiplexed over TCP), and HTTP/3 (over QUIC/UDP) parsers. The transport layer is plug-in: by default it's the managed Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.
The request pipeline is built once at Build(). Each request hits an internal connection processor that parses the request, builds an HttpContext, and dispatches into the middleware chain.
IHostApplicationLifetime is a singleton service that wraps three CancellationTokens plus a StopApplication() method. SIGTERM / Ctrl+C handlers in the host call StopApplication().
Code: correct vs wrong
❌ Wrong: blocking startup
public class WarmupService(IRepository repo) : IHostedService
{
public async Task StartAsync(CancellationToken ct)
{
await repo.LoadAllAsync(ct); // ❌ takes 60s; listener doesn't bind for 60s
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
✅ Correct: long work in BackgroundService
public class WarmupService(IRepository repo) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stop)
{
await repo.LoadAllAsync(stop); // runs after host is up
}
}
❌ Wrong: ignoring MaxConcurrentConnections
✅ Correct: cap
builder.WebHost.ConfigureKestrel(o =>
{
o.Limits.MaxConcurrentConnections = 5000;
o.Limits.MaxConcurrentUpgradedConnections = 1000;
});
❌ Wrong: trusting X-Forwarded-For from any IP
✅ Correct: configure trust
builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
o.ForwardedHeaders = ForwardedHeaders.All;
o.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
});
Design patterns for this topic
Pattern 1 — "Generic host for everything"
- Intent: unified hosting for web, console, workers.
- Code sketch:
Host.CreateApplicationBuilder()for non-web;WebApplication.CreateBuilder()for web — both yieldIHost.
Pattern 2 — "Long-running work in BackgroundService"
- Intent: non-blocking startup; cooperative shutdown.
Pattern 3 — "Kestrel direct in containers"
- Intent: simplest production topology; Ingress is your edge.
Pattern 4 — "Slim builder for AOT"
- Intent: smaller binaries; faster startup.
Pattern 5 — "Graceful shutdown with IHostApplicationLifetime"
- Intent: finish in-flight work before exit.
Pros & cons / trade-offs
| Choice | Pros | Cons |
|---|---|---|
| Kestrel direct | Simpler; faster | Edge concerns on you |
| Behind reverse proxy | Centralized edge | One more hop |
IHostedService | Standard lifecycle | Blocks startup |
BackgroundService | Non-blocking | Manual loop / cancellation |
CreateSlimBuilder | AOT-friendly; small | Fewer defaults; manual setup |
When to use / when to avoid
- Use
WebApplication.CreateBuilderfor web apps in 2026; it's the modern default. - Use
BackgroundServicefor long-running work. - Avoid blocking
StartAsync— listener doesn't bind until it returns. - Avoid Kestrel without limits in production.
- Avoid
UseForwardedHeaderswithoutKnownProxies— spoofable.
Interview Q&A
Q1. What's the relationship between IHost and WebApplication? WebApplication is an IHost with web-specific extensions. IHost is the unified abstraction; WebApplication adds routing, middleware, Kestrel.
Q2. What signals graceful shutdown? SIGTERM (Linux) or Ctrl+C (Windows). The host catches and triggers IHostApplicationLifetime.ApplicationStopping. Hosted services receive cancellation via their StopAsync token.
Q3. What's the default shutdown timeout? 30 seconds. Configure via HostOptions.ShutdownTimeout. Match to terminationGracePeriodSeconds in K8s.
Q4. Difference between IHostedService and BackgroundService? IHostedService has explicit StartAsync/StopAsync. BackgroundService is a base class implementing IHostedService where you override ExecuteAsync for long-running work.
Q5. Why might StartAsync block startup? Because the host calls StartAsync on each hosted service in order before the listener binds. Use BackgroundService.ExecuteAsync for work that should not delay startup.
Q6. When should you use CreateSlimBuilder? For NativeAOT or minimal microservices where the default service registrations are unnecessary overhead.
Q7. What's UseForwardedHeaders for? Promotes X-Forwarded-For / X-Forwarded-Proto headers from a trusted proxy into HttpContext.Connection. Required for accurate client IPs / scheme behind a proxy.
Q8. How do you secure UseForwardedHeaders? Configure KnownProxies / KnownNetworks. Untrusted forwarders are ignored. Otherwise clients can spoof their IP.
Q9. What's Kestrel's MinRequestBodyDataRate? A floor on incoming-body throughput. If a client sends slower than the threshold, Kestrel kills the connection. Defense against slow-loris-style attacks.
Q10. What's the difference between Kestrel and IIS / NGINX as an ASP.NET Core server? Kestrel is the actual .NET server. IIS and NGINX (in classic deployments) act as reverse proxies or in-process hosts (IIS in-process via ANCM). Modern deployments usually run Kestrel directly behind a Layer 7 proxy.
Q11. When do you choose HTTP/3? When clients are mobile or on lossy networks (HTTP/3's QUIC handles packet loss better than TCP). Set Protocols = HttpProtocols.Http1AndHttp2AndHttp3 and ensure your edge supports it.
Q12. What's BackgroundServiceExceptionBehavior? Controls what happens when BackgroundService.ExecuteAsync throws. Options: Ignore (just log), StopHost (kill the whole app). Default in modern .NET is StopHost — surface failures.
Gotchas / common mistakes
- ⚠️ Blocking
StartAsync— startup hangs. - ⚠️ Unlimited Kestrel connections — OOM under flood.
- ⚠️
UseForwardedHeaderswithoutKnownProxies— header spoofing. - ⚠️ Long graceful shutdown timeout outliving K8s grace period — pod gets
SIGKILL'd mid-shutdown. - ⚠️
Task.DelayinBackgroundServicewithout cancellation — can hang shutdown for full delay duration. - ⚠️ Logging in
IHostApplicationLifetimeevents — ensure logger is still alive during shutdown.