.NET health check endpoint in ASP.NET Core

ASP.NET Core has health checks built in: AddHealthChecks() registers them, MapHealthChecks("/health") serves them, and an unhealthy result returns 503 with no extra configuration.

Health checks ship with the framework, so the basic case needs no package at all. Call builder.Services.AddHealthChecks() before the app is built and app.MapHealthChecks("/health") after, and you have an endpoint that answers 200 with the plaintext body Healthy. On its own that endpoint proves Kestrel accepted a connection, which you already knew, because something answered.

It becomes useful the moment it touches what the request path needs. Add the EntityFrameworkCore package from Microsoft.Extensions.Diagnostics.HealthChecks and AddDbContextCheck<AppDbContext>() calls CanConnectAsync on your context each time the endpoint is hit. Now a process that is running but cannot reach its database reports differently from a healthy one, which is the entire point. The status mapping is already what you want: Healthy and Degraded return 200, Unhealthy returns 503. Since Logdash flips a monitor to down on anything outside 200 to 399, an unhealthy .NET app becomes an alert without you setting a single threshold.

The whole thing in Program.cs

Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("db");

var app = builder.Build();

// Unhealthy maps to 503; the default body is one word
app.MapHealthChecks("/health");

app.Run();

What to check and what to leave out

  • One check per dependency you cannot serve without. AddDbContextCheck is that check for EF Core, so there is no reason to hand-roll a second query alongside it.
  • Keep the whole endpoint under a second. Every registered check runs on every request, so eight checks with a 200 ms budget each is an endpoint that takes longer than a page load.
  • No calls to third-party APIs. Their outage is not your outage, and you do not want to be woken up for it.
  • Watch what the body says. The default response writer prints Healthy or Unhealthy and nothing else, which is exactly right for a public URL. The JSON writers people paste in from samples serialise every entry name, description, duration and exception message.
  • If you want the detailed body, map a second route with a custom ResponseWriter and put RequireAuthorization() on it. Leave the public one plain.

The UI package, honestly

AspNetCore.HealthChecks.UI is the community dashboard, part of the Xabaril AspNetCore.Diagnostics.HealthChecks project, and Microsoft states plainly that it does not maintain or support it. It is a real option and plenty of teams run it: add the UI package and a storage provider, list your endpoints in configuration, get a page with history. What you are also adding is a second ASP.NET app, a database for the results, and a dashboard that usually sits in the same cluster as the service it watches, so it goes dark in the same incident. That is the argument for checking the endpoint from outside your infrastructure as well.

Aspire is worth the same note. Its service defaults call MapDefaultEndpoints, which maps /health for readiness and /alive for the checks tagged live, but only in the Development environment, because the template treats those endpoints as something to protect in production. The Aspire dashboard is a development tool. Neither of them is watching your deployed app at 3am, so a deployed service still needs a monitor pointed at a URL that a stranger on the internet can resolve.

Point a monitor at it

  1. 1
    Register the endpoint Deploy with MapHealthChecks("/health") in place and confirm the URL answers from outside your network before you automate anything. Curl it with -i and read the status line, not the body.
  2. 2
    Create the monitor Add a project in Logdash, paste the health URL, and every check from then on records the status code and the response time. The free plan checks every 5 minutes, Builder every minute, Pro every 15 seconds, and the pinger gives up after 10 seconds.
  3. 3
    Break the connection string Point the context at a database that is not there and redeploy. CanConnectAsync fails, the report comes back Unhealthy, the endpoint returns 503, and the Telegram alert arrives with the URL and the code on the next scheduled check.
What is the default health check endpoint in ASP.NET Core?

There is no default path. You choose it in MapHealthChecks, and /health is the convention almost everyone follows. Aspire service defaults use /health for readiness and /alive for liveness.

Which package provides the EF Core health check?

The EntityFrameworkCore package under Microsoft.Extensions.Diagnostics.HealthChecks, maintained by Microsoft. It adds AddDbContextCheck<TContext>(), which runs CanConnectAsync unless you pass a customTestQuery of your own.

Is AspNetCore.HealthChecks.UI worth adding?

It is a good dashboard and a community project Microsoft does not support. It also runs inside your own infrastructure, so it cannot tell you about an outage that takes the cluster with it.

Should the health check API be authenticated?

The plain endpoint can stay open because its body is one word and an external monitor has to reach it. Authenticate any route that returns the detailed JSON report, and consider host filtering on the management route.

What status code does a failed health check return?

503. The default mapping is Healthy and Degraded to 200 and Unhealthy to 503, which is what makes the endpoint alertable without any extra plumbing on the monitoring side.

Point it at your own URL and watch it for real.

Any public URL · checked every 15 s