Hosting & the Kestrel Server
Your code never talks to the network directly. A host starts your app, owns its services, configuration and logging, starts a web server, and shuts everything down cleanly when the process is asked to stop. In ASP.NET Core the server is almost always Kestrel.
In this lesson you will learn what the host does, how to control the URLs and ports Kestrel listens on, why production apps usually sit behind a reverse proxy, and how to run a background job, such as appointment reminders, inside the same host.
The host and the server
When WebApplication.CreateBuilder(args) runs, it creates a host with these parts already wired together:
- Configuration: values from
appsettings.json, environment variables and the command line. - Logging: providers such as the console.
- Dependency injection: the service container.
- The server: Kestrel, unless the app is hosted inside IIS.
- Lifetime management: start-up, Ctrl+C handling and graceful shutdown.
The server is the part that opens a network port, reads raw HTTP bytes, builds an HttpContext for each request and hands it to your middleware. ASP.NET Core offers three options:
| Server | Platforms | When it is used |
|---|---|---|
| Kestrel | Windows, Linux, macOS | The default and recommended server, everywhere |
| IIS HTTP Server (in-process) | Windows | When the app is hosted inside IIS |
| HTTP.sys | Windows | Rare: Windows-only features such as kernel-mode Windows authentication |
Kestrel supports HTTP/1.1, HTTP/2, HTTP/3 (which requires HTTPS) and WebSockets. It is fast enough to face the internet directly, but most production setups still put a reverse proxy in front of it.
Choosing URLs and ports
If nothing configures an address, Kestrel listens on http://localhost:5000. During development, launchSettings.json supplies the URLs instead. You can choose the address in several ways; pick one per environment rather than mixing them.
# 1. Command-line argument
dotnet run --urls "http://localhost:6000"
# 2. Environment variable with full URLs (Linux/macOS shell)
ASPNETCORE_URLS="http://0.0.0.0:8080" dotnet ClinicApi.dll
# 3. Environment variable with only port numbers (any host name)
ASPNETCORE_HTTP_PORTS=8080 dotnet ClinicApi.dlllocalhost accepts connections only from the same machine. 0.0.0.0 (or *) listens on every network interface, which you need inside a container or when another computer must reach the app. The official .NET container images set ASPNETCORE_HTTP_PORTS=8080, which is why a containerised app listens on port 8080.
You can also configure endpoints in appsettings.json, which suits servers where settings live in files:
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:5080"
}
}
}
}When the app starts, the log confirms the address:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://0.0.0.0:5080
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.Reverse proxies
A reverse proxy is a web server that receives requests from the internet and forwards them to your app. On Linux this is usually Nginx or Apache; on Windows it is IIS; in the cloud it is often a load balancer.
Browser --HTTPS--> Nginx (port 443) --HTTP--> Kestrel (127.0.0.1:5000) --> your appA proxy is useful because it can terminate HTTPS with your real certificate, serve several sites on one machine and one port, balance load across copies of the app, and apply limits before traffic reaches .NET. Because the proxy now makes the request to Kestrel, your app sees the proxy's address unless you add the Forwarded Headers middleware, which reads the X-Forwarded-For and X-Forwarded-Proto headers that the proxy adds. You will set this up in Deployment.
Tuning Kestrel
Kestrel has safe defaults, such as a maximum request body of about 28.6 MB (30,000,000 bytes). You can change limits in code through builder.WebHost:
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
// Prescription photos are small; refuse anything over 5 MB
options.Limits.MaxRequestBodySize = 5 * 1024 * 1024;
options.Limits.MaxConcurrentConnections = 500;
});
var app = builder.Build();
app.MapGet("/", () => "Kestrel limits configured");
app.Run();A request with a larger body is rejected with status code 413 before your code reads it. Lower limits protect a small server from being flooded with huge uploads.
Start-up and graceful shutdown
When you press Ctrl+C, or a server sends the process a SIGTERM signal, the host does not simply die. It stops accepting new requests, waits for requests already running (up to a timeout), stops background services and then exits. This graceful shutdown is why deployments do not cut off a patient in the middle of booking.
You can run code at these moments through IHostApplicationLifetime:
var app = builder.Build();
app.Lifetime.ApplicationStarted.Register(() =>
app.Logger.LogInformation("Clinic API is ready"));
app.Lifetime.ApplicationStopping.Register(() =>
app.Logger.LogInformation("Clinic API is stopping"));
app.Run();Background jobs with BackgroundService
Many apps need work that is not triggered by a request: sending reminders, cleaning old records, importing a file every night. The host can run hosted services alongside the web server. The easiest way is to inherit from BackgroundService and register it:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<ReminderWorker>();
var app = builder.Build();
app.MapGet("/", () => "Clinic API with reminders");
app.Run();
class ReminderWorker(ILogger<ReminderWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
logger.LogInformation("Checking for appointments that need a reminder");
}
}
}ExecuteAsync starts when the host starts. PeriodicTimer waits five minutes between runs without blocking a thread, and stoppingToken is cancelled during shutdown, which ends the loop. Every five minutes the console shows:
info: ReminderWorker[0]
Checking for appointments that need a reminderA hosted service is a singleton, so it must not keep a scoped service such as a database context for its whole life. Create a scope for each run instead; you will see how in Dependency Injection & Lifetimes. For heavy or critical jobs (thousands of emails, payment retries), a dedicated worker project or a job library is often a better home than the web app.
Common mistakes
- Listening on
localhostinside a container. Nothing outside the container can connect. Use0.0.0.0orASPNETCORE_HTTP_PORTS. - Using
Thread.Sleepin a background service. It blocks a thread and ignores shutdown. UsePeriodicTimerorTask.Delaywith the stopping token. - Killing the process with
SIGKILL. It skips graceful shutdown. Let the service manager sendSIGTERM(the default for systemd and Docker). - Forgetting forwarded headers behind a proxy. Redirects then point to
http://and logs show the proxy's IP for every user.
Key points
- The host owns configuration, logging, DI, the server and the app's lifetime;
WebApplicationcreates it for you. - Kestrel is the default cross-platform server; IIS in-process hosting and HTTP.sys are Windows options.
- Set addresses with
--urls,ASPNETCORE_URLS,ASPNETCORE_HTTP_PORTSor theKestrel:Endpointsconfiguration section. - Production apps usually sit behind a reverse proxy such as Nginx or IIS, with forwarded headers enabled.
BackgroundServiceruns long-lived jobs in the same host and stops cleanly through the cancellation token.
Exercise
Write a Program.cs for a canteen in Kochi that listens on port 7070 on all network interfaces (set in code through configuration, not on the command line), limits request bodies to 1 MB, and runs a MenuRefresher background service that logs "Refreshing today's menu" every 30 seconds.
Show solution
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://0.0.0.0:7070");
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 1024 * 1024);
builder.Services.AddHostedService<MenuRefresher>();
var app = builder.Build();
app.MapGet("/", () => "Kochi canteen API");
app.Run();
class MenuRefresher(ILogger<MenuRefresher> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
logger.LogInformation("Refreshing today's menu");
}
}
}UseUrls sets the address from code; an ASPNETCORE_URLS variable or a Kestrel:Endpoints section would also work, and keeping the port in configuration is more flexible for real deployments.
Quick Quiz
1. If nothing configures an address, where does Kestrel listen?
With no endpoint configuration Kestrel binds to http://localhost:5000.
2. Why must a containerised app listen on 0.0.0.0 (or use ASPNETCORE_HTTP_PORTS) instead of localhost?
Requests from outside the container arrive on its network interface, not on its loopback address.
3. What is the main job of a reverse proxy such as Nginx in front of Kestrel?
The proxy faces the internet and forwards requests to Kestrel, often adding TLS and load balancing.
4. How should a BackgroundService wait between runs?
Awaiting with the token frees the thread and lets the host stop the service cleanly.
Learn ASP.NET Core with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
