Kestrel is the web server inside every ASP.NET Core application — the process that accepts connections, parses HTTP, and hands requests to your middleware pipeline. It's what dotnet run starts and what serves production traffic behind a reverse proxy. This tutorial covers how Kestrel fits into your app, configuring endpoints and URLs, tuning limits (verified with real requests), HTTPS, and the one thing the docs bury: which settings work from appsettings.json and which must be code.

What is Kestrel in .NET Core?
Kestrel is the cross-platform web server built into ASP.NET Core — the process that actually listens on the socket, parses HTTP, and hands requests to your middleware pipeline. Every ASP.NET Core app runs on Kestrel by default, whether it faces the internet directly or sits behind IIS, Nginx, or a container ingress. It supports HTTP/1.1, HTTP/2, and HTTP/3, TLS termination, and is one of the fastest web servers in independent benchmarks — which is why the usual production shape is a lightweight reverse proxy in front and Kestrel doing the real work.
Where Kestrel sits
Internet → reverse proxy (Caddy/Nginx/IIS) → Kestrel → your middleware → controller
Kestrel is fast and production-grade, but the usual deployment keeps a reverse proxy in front for TLS termination, compression, and serving multiple apps on one server — this platform's own deployment runs Caddy in front of Kestrel in Docker. Kestrel directly on the internet is supported, but then TLS, rate limiting, and header hardening are all your job.
URLs and endpoints
Which addresses Kestrel listens on, in order of precedence:
# 1. command line
dotnet run --urls "http://localhost:5080"
# 2. environment variable (how containers do it)
ASPNETCORE_URLS=http://0.0.0.0:8080
# 3. appsettings.json
{
"Kestrel": {
"Endpoints": {
"Http": { "Url": "http://localhost:5080" },
"Https": { "Url": "https://localhost:5443" }
}
}
}
0.0.0.0 binds all interfaces — required in Docker, where localhost would be unreachable from outside the container.
Limits — and the appsettings trap
Kestrel enforces protective limits per connection and request. Here's the catch, found the hard way while verifying this article: the Kestrel section of appsettings.json configures endpoints and certificates — the Limits you put there are silently ignored. Limits are set in code:
builder.WebHost.ConfigureKestrel(options =>
{
options.AddServerHeader = false; // don't advertise "Server: Kestrel"
options.Limits.MaxRequestBodySize = 1024; // bytes; null = unlimited
options.Limits.MaxConcurrentConnections = 100;
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
Verified behavior with the 1 KB body limit:
POST 500-byte body → 200 {"received":500}
POST 2 KB body → 413 Payload Too Large
GET / → no "Server" header in the response
The defaults are sensible (30 MB max body, 100-header limit, 2-minute keep-alive); change them deliberately:
MaxRequestBodySize— raise for upload endpoints (or per-action with[RequestSizeLimit]), lower to shed abuse early.AddServerHeader = false— removes the server banner; trivial hardening with zero cost.MaxConcurrentConnections— usually leftnull(unlimited); the thread pool, not connections, is the real bottleneck.- Timeouts — protect against slow-loris-style clients holding connections open.
Per-endpoint override beats the global setting:
[RequestSizeLimit(50_000_000)] // this upload action allows 50 MB
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file) { … }
HTTPS in development and production
dotnet dev-certs https --trust sets up the local development certificate — the template's https profile uses it automatically.
In production, prefer terminating TLS at the reverse proxy (Caddy provisions Let's Encrypt certificates automatically; Nginx uses certbot). If Kestrel must terminate TLS itself:
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://0.0.0.0:443",
"Certificate": { "Path": "/certs/site.pfx", "Password": "…" }
}
}
}
Behind a proxy, add the forwarded-headers middleware so the app sees the original scheme and client IP:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
Protocols
HTTP/2 is on by default for HTTPS endpoints. HTTP/3 is one line:
"Endpoints": {
"Https": { "Url": "https://0.0.0.0:443", "Protocols": "Http1AndHttp2AndHttp3" }
}
Watching Kestrel work: logging
Two switches make Kestrel observable when something's off:
"Logging": {
"LogLevel": {
"Microsoft.AspNetCore.Hosting.Diagnostics": "Information",
"Microsoft.AspNetCore.Server.Kestrel": "Debug"
}
}
Hosting.Diagnostics at Information logs one line per request with method, path, status, and elapsed time. Kestrel at Debug adds connection-level events — TLS handshakes, resets, limit rejections — which is where you see why a client got a 413 or a connection was dropped. For request/response bodies during debugging there's app.UseHttpLogging(), but keep it out of production (it buffers bodies and logs sensitive data).
Graceful shutdown
When the host receives SIGTERM (a docker stop, a deployment), it stops accepting new connections and gives in-flight requests a grace window — 30 seconds by default:
builder.Services.Configure<HostOptions>(o =>
o.ShutdownTimeout = TimeSpan.FromSeconds(10));
Containers should finish fast: orchestrators send SIGKILL after their own timeout, and a request that outlives both is simply cut. Pass CancellationTokens through your handlers (they're signaled on shutdown) so long queries stop cleanly instead of being severed mid-write.
Systemd and unix sockets
On a Linux VM without containers, run Kestrel as a systemd service:
[Service]
WorkingDirectory=/opt/geekstore
ExecStart=/usr/bin/dotnet /opt/geekstore/GeekStore.Web.dll
Restart=always
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Binding 127.0.0.1 keeps Kestrel private to the box; Nginx proxies from :443. For slightly lower overhead between proxy and app on the same host, Kestrel can listen on a unix socket instead of TCP: ASPNETCORE_URLS=http://unix:/run/geekstore.sock.
Checklist for a production app
- Bind
0.0.0.0in containers; keep the port an env var (ASPNETCORE_URLS). - Terminate TLS at the proxy; enable forwarded headers.
- Set
AddServerHeader = false; reviewMaxRequestBodySizeper endpoint. - Remember: endpoints in config, limits in code.
Comments (0)
No comments yet — be the first to share your thoughts.