Skip to content

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 by IHostApplicationLifetime.ApplicationStopping — wired automatically to SIGTERM / 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.
  • UseUrls vs Listen / ListenAnyIp — the latter give per-endpoint control (transport, certificate, HTTP version).
  • ConfigureKestrel is 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:

  1. Configuration — loads appsettings.json, environment variables, command-line args, secrets.
  2. Logging — bootstraps ILogger<T> from configured providers.
  3. DI container — builds the service provider; registers IHostedServices.
  4. Hosted services lifecycle — calls StartAsync on each in registration order; calls StopAsync in reverse on shutdown.
  5. Application lifetime eventsApplicationStarted, 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: MaxConcurrentConnections defaults 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.

⚠️ StartAsync blocks startup. If it takes 30s, the listener doesn't bind for 30s. Either keep it fast or move work into BackgroundService.ExecuteAsync (which runs in the background after Start returns).

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

// no Kestrel limits — defaults to unlimited; SYN flood → OOM

✅ Correct: cap

builder.WebHost.ConfigureKestrel(o =>
{
    o.Limits.MaxConcurrentConnections = 5000;
    o.Limits.MaxConcurrentUpgradedConnections = 1000;
});

❌ Wrong: trusting X-Forwarded-For from any IP

app.UseForwardedHeaders();   // no KnownProxies — anyone can spoof

✅ 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 yield IHost.

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.CreateBuilder for web apps in 2026; it's the modern default.
  • Use BackgroundService for long-running work.
  • Avoid blocking StartAsync — listener doesn't bind until it returns.
  • Avoid Kestrel without limits in production.
  • Avoid UseForwardedHeaders without KnownProxies — 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 connectionsOOM under flood.
  • ⚠️ UseForwardedHeaders without KnownProxies — header spoofing.
  • ⚠️ Long graceful shutdown timeout outliving K8s grace period — pod gets SIGKILL'd mid-shutdown.
  • ⚠️ Task.Delay in BackgroundService without cancellation — can hang shutdown for full delay duration.
  • ⚠️ Logging in IHostApplicationLifetime events — ensure logger is still alive during shutdown.

Further reading