Module 1 · Hosting and the Request Pipeline · Lesson 1 of 18
How an ASP.NET Core App Starts: WebApplication, the Generic Host and Kestrel
Learning outcome
By the end of this lesson you can explain what WebApplication.CreateBuilder actually builds, in what order the host starts its pieces, where Kestrel fits, and what "graceful shutdown" really does. That is the backbone every later lesson hangs on, and it is the first thing a senior interviewer probes with "walk me through what happens when the app starts".
How this course works
Examples target .NET 10 (SDK 10.0.401, ASP.NET Core runtime 10.0.12). Most HTTP demonstrations start Kestrel on a free local port and call it with HttpClient; DI/configuration probes and the TestServer lesson use different harnesses. Output blocks illustrate the expected behavior. Environment, configuration, scheduling, trace ids, connection ids and timings can affect the output.
The course goes deep on internals: what runs, in what order, and why. Where TechInterviewNotes already has a focused course (security, caching, integration testing), this course links to it instead of repeating it.
Intuition
An ASP.NET Core app is three layers:
- The Generic Host (
IHost): owns DI, configuration, logging and the lifetime of everything (start, stop, hosted services). - The web host service (
GenericWebHostService): a hosted service like any other. When the host starts it, it builds the middleware pipeline and starts the server. - The server (
IServer, normally Kestrel): owns sockets and turns bytes intoHttpContextobjects, then calls the pipeline (a singleRequestDelegate).
flowchart TD
A["WebApplication.CreateBuilder(args)"] --> B["config + logging + DI registrations"]
B --> C["builder.Build(): ServiceProvider created"]
C --> D["app.Use / app.Map...: pipeline described"]
D --> E["app.Run / StartAsync"]
E --> F["your IHostedService.StartAsync (in registration order)"]
F --> G["GenericWebHostService.StartAsync: build RequestDelegate, start Kestrel"]
G --> H["ApplicationStarted"]
H --> I["requests flow: Kestrel -> HttpContext -> pipeline"]Deep dive
What CreateBuilder sets up
WebApplication.CreateBuilder(args) gives you defaults that are easy to forget in an interview:
- Configuration sources, in order:
appsettings.json,appsettings.{Environment}.json, user secrets (Development only), environment variables, command-line arguments. Later sources override earlier ones (Lesson 6). - Logging providers: Console, Debug, EventSource (and EventLog on Windows).
- Kestrel as the server, configured from the
Kestrelconfiguration section,ASPNETCORE_URLS,ASPNETCORE_HTTP_PORTS,--urlsorUseUrls. - DI validation in Development:
ValidateScopesandValidateOnBuildare switched on, which catches captive dependencies at startup (Lesson 5). - Automatic middleware: if you map endpoints and do not call them yourself,
WebApplicationaddsUseRoutingat the start andUseEndpointsat the end;UseAuthentication/UseAuthorizationright after routing when those services are registered; and the developer exception page first in Development (Lesson 2).
Build vs Start
The ordering below assumes the default sequential hosted-service startup and shutdown. HostOptions.ServicesStartConcurrently and ServicesStopConcurrently can change completion ordering.
builder.Build() creates the service provider and freezes registrations. Nothing is listening yet. app.Run() (or StartAsync) starts the host, which starts each IHostedService in registration order. The web host service is registered after your services, so your hosted services start before Kestrel binds its ports. On shutdown the order is reversed: Kestrel stops first, then your hosted services.
What Kestrel does
Kestrel is a cross-platform, managed HTTP server. It accepts connections (sockets transport by default), parses HTTP/1.1, HTTP/2 and HTTP/3, enforces limits (request body size, header size, connection counts, timeouts) and creates the HttpContext for each request. It then calls your pipeline on a thread-pool thread. It can run on its own at the edge or behind a reverse proxy (IIS, Nginx, YARP, a cloud load balancer). Behind a proxy, configure the forwarded headers to process and the trusted proxies/networks, then add UseForwardedHeaders so the app sees the forwarded client scheme and IP.
Graceful shutdown
On SIGTERM or Ctrl+C (or StopAsync), the host fires ApplicationStopping, Kestrel stops accepting new connections and waits for in-flight requests, then hosted services are stopped, then ApplicationStopped fires. HostOptions.ShutdownTimeout (30 seconds by default) cancels the shutdown token. This is cooperative, not a process-kill deadline: custom stop code that ignores cancellation can exceed it. Orchestrators such as Kubernetes have their own grace period; it must be longer than your shutdown timeout or the process is killed mid-drain.
Code example: watch the host start and stop
// L01: what WebApplication builds, how Kestrel binds, and the host lifecycle order.
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // keep the console output readable
builder.WebHost.UseUrls("http://127.0.0.1:0"); // port 0 = let the OS pick a free port
builder.Services.AddHostedService<StartupProbe>();
var app = builder.Build();
var lifetime = app.Lifetime;
lifetime.ApplicationStarted.Register(() => Console.WriteLine("2. ApplicationStarted fired"));
lifetime.ApplicationStopping.Register(() => Console.WriteLine("4. ApplicationStopping fired"));
lifetime.ApplicationStopped.Register(() => Console.WriteLine("6. ApplicationStopped fired"));
app.MapGet("/", (HttpContext ctx) =>
{
var conn = ctx.Features.Get<IHttpConnectionFeature>()!;
return $"protocol={ctx.Request.Protocol} thread-pool={Thread.CurrentThread.IsThreadPoolThread} " +
$"connection-id-set={!string.IsNullOrEmpty(conn.ConnectionId)}";
});
Console.WriteLine($"Environment: {app.Environment.EnvironmentName}");
Console.WriteLine($"IServer implementation: {app.Services.GetRequiredService<IServer>().GetType().Name}");
await app.StartAsync(); // runs hosted services, then starts Kestrel
var address = app.Services.GetRequiredService<IServer>().Features
.Get<IServerAddressesFeature>()!.Addresses.First();
Console.WriteLine($"3. Kestrel listening; port chosen by OS: {new Uri(address).Port > 0}");
using var http = new HttpClient { BaseAddress = new Uri(address) };
Console.WriteLine(" GET / -> " + await http.GetStringAsync("/"));
await app.StopAsync(); // graceful: stop accepting, drain, stop hosted services
await app.DisposeAsync();
sealed class StartupProbe(IServer server) : IHostedService
{
public Task StartAsync(CancellationToken ct)
{
var bound = server.Features.Get<IServerAddressesFeature>()!.Addresses.Count;
Console.WriteLine($"1. IHostedService.StartAsync: Kestrel addresses bound so far = {bound}");
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken ct)
{
var address = server.Features.Get<IServerAddressesFeature>()!.Addresses.First();
using var http = new HttpClient { BaseAddress = new Uri(address) };
try { await http.GetAsync("/"); Console.WriteLine("5. IHostedService.StopAsync: server still answering"); }
catch (HttpRequestException) { Console.WriteLine("5. IHostedService.StopAsync: Kestrel already stopped (connection refused)"); }
}
}Output
Environment: Production IServer implementation: KestrelServerImpl 1. IHostedService.StartAsync: Kestrel addresses bound so far = 0 2. ApplicationStarted fired 3. Kestrel listening; port chosen by OS: True GET / -> protocol=HTTP/1.1 thread-pool=True connection-id-set=True 4. ApplicationStopping fired 5. IHostedService.StopAsync: Kestrel already stopped (connection refused) 6. ApplicationStopped fired
Code walkthrough
IServer implementation: KestrelServerImplproves the server is just a service in the container. Swapping it (HTTP.sys,TestServerin Lesson 16) changes nothing in your pipeline.- Step 1: when our hosted service starts, Kestrel has 0 addresses. Our service ran before
GenericWebHostService. - Step 3: after
StartAsync, the OS has picked a port (we asked for port 0). - The request ran on a thread-pool thread, over HTTP/1.1 on a connection with an id. That id appears again in logs in Lesson 11.
- Step 5: when our hosted service is stopped, a request to the server is refused. Kestrel stopped first, exactly the reverse of startup.
Summary table
| Phase | What happens | Key type |
|---|---|---|
| CreateBuilder | Config sources, logging, Kestrel defaults, DI collection | WebApplicationBuilder |
| Build | Service provider created; registrations frozen | WebApplication, IServiceProvider |
| Pipeline description | Use, Map, MapGet... recorded, not executed | IApplicationBuilder |
| StartAsync | Hosted services start in order; web host builds the pipeline and starts the server | IHostedService, GenericWebHostService |
| Running | Server creates HttpContext, calls the RequestDelegate | IServer, HttpContext |
| StopAsync | Stopping event, server drains, hosted services stop (reverse), Stopped event | IHostApplicationLifetime, HostOptions |
Failure modes
- Doing slow work in a hosted service's
StartAsync: Kestrel does not listen until it returns. Readiness probes fail and deployments time out. - Resolving scoped services from
app.Servicesat startup without creating a scope: it throws in Development and silently creates a long-lived instance in Production. - A shutdown timeout longer than the orchestrator's grace period: the process is killed while requests are still draining.
- Behind a proxy without forwarded headers: redirects use
http, and logs show the proxy's IP instead of the client's.
Interview angle
"What happens between dotnet run and the first request?" A strong answer names the layers: the builder sets up configuration, logging and DI; Build creates the container; Run starts hosted services in order, and the web host service then builds the middleware pipeline into one RequestDelegate and starts Kestrel; Kestrel creates an HttpContext per request and calls that delegate. Add the shutdown order, and mention that your hosted services start before the server listens. That detail separates people who have read the source from people who have only used templates.
Practice questions
- Is Kestrel listening when
builder.Build()returns?
Answer: No. Build only creates the container. The server starts inside StartAsync/Run, when the host starts GenericWebHostService.
- Your hosted service's
StartAsyncloads a 2 GB cache synchronously. What is the user-visible effect?
Answer: The app does not accept connections until it finishes, because the web host service starts after it. Move the load into a BackgroundService (Lesson 14) or into a readiness-gated warm-up.
- Which defaults does
CreateBuilderturn on only in Development?
Answer: User secrets, the developer exception page, and DI scope validation (ValidateScopes and ValidateOnBuild).
- What bounds graceful shutdown, and what is its default?
Answer: HostOptions.ShutdownTimeout cancels the shutdown token after 30 seconds by default. Stop code must cooperate; the timeout itself does not kill the process.
Revision checklist
- Generic Host owns DI, config, logging and lifetime; the web host is one hosted service; Kestrel is an
IServer. Buildcreates the container;Run/StartAsyncstarts things.- Your hosted services start before Kestrel and stop after it.
- Shutdown: Stopping, drain, hosted services stop, Stopped;
ShutdownTimeout(30 s) requests cancellation, not forced termination of noncooperative stop callbacks.
Related on TechInterviewNotes
- Free note: How an ASP.NET Core Request Actually Flows. It is a short overview; this course is the deep version.
- Docker from zero to practical .NET 10 for container ports, health checks and stop signals.
Analogy
A restaurant opening for the day
The host is the restaurant manager. Hosted services are the prep cooks who arrive first. The web host service unlocks the front door (Kestrel) only after prep is done. At closing time the door is locked first, the last diners finish (draining), and then the kitchen shuts down. The manager gives everyone 30 minutes (the shutdown timeout) before turning off the lights.
Quick reference
Startup and shutdown order
- CreateBuilder: config, logging, Kestrel, DI collection.
- Build: container created.
- Start: your hosted services, then the web host (pipeline built, Kestrel listening), then
ApplicationStarted. - Stop:
ApplicationStopping, Kestrel drains and stops, hosted services stop in reverse order,ApplicationStopped. - Default shutdown timeout: 30 seconds.