Skip to main content
SEMastery
DevOpsbeginner

Health Checks in ASP.NET Core: A Beginner's Guide

Learn health checks in ASP.NET Core: add liveness and readiness endpoints, check your database and Redis, write custom checks, and wire up Kubernetes probes.

13 min readUpdated December 6, 2025

A doctor's quick check-up

Think about going to a doctor for a routine check-up. The doctor does not run every test in the hospital. They do a few quick things: check your pulse, listen to your heart, look at your breathing. In a minute they can say "you are fine," "you are okay but rest a bit," or "we need to act now."

A health check in ASP.NET Core is exactly this kind of quick check-up, but for your web app. Instead of a doctor, the one doing the check-up is a robot — maybe Kubernetes, a load balancer, or an uptime monitor. It visits a special URL on your app every few seconds and asks one simple question: "Are you okay?"

Your app runs a few fast checks — can it reach the database, is Redis up, is there enough disk space — and replies with one of three answers: Healthy, Degraded, or Unhealthy. The robot then decides what to do: keep sending traffic, slow down, or restart the app.

This guide teaches you how to set this up from zero, in plain steps. By the end you will be able to add liveness and readiness endpoints, check your database, write your own custom check, and connect everything to Kubernetes.

Why health checks matter

Imagine your shop's website is running, but its database connection quietly broke. The website still says "200 OK" on the home page because that page is cached. But every checkout fails. Customers leave. Nobody notices for an hour.

A health check would have caught this in seconds. The robot would have asked "are you okay?", your app would have tried to reach the database, failed, and answered Unhealthy. The load balancer would then stop sending traffic to that broken copy and route people to a healthy one.

Figure 1: A monitor pings the health URL. The app checks its parts and answers. The monitor acts on the answer.

The big idea: a health check turns "is my app okay?" from a guess into a clear, machine-readable answer.

The three health states

ASP.NET Core uses three statuses. Learn these and the rest is easy.

StatusMeaningDefault HTTP codeReal-life example
HealthyEverything works fine200 OKDatabase reachable, cache fast
DegradedWorks, but not great200 OKCache slow, app falls back to DB
UnhealthyCannot do its job503 Service UnavailableDatabase is unreachable

Notice that Degraded still returns 200 by default. The app is up, just not at its best. Only Unhealthy returns 503, which tells a load balancer to stop sending traffic.

Your very first health check

Let us add the simplest possible health check. The core API ships with ASP.NET Core, so you do not need any extra NuGet package for this.

Open your Program.cs and add two lines.

var builder = WebApplication.CreateBuilder(args);
 
// 1) Register the health check service
builder.Services.AddHealthChecks();
 
var app = builder.Build();
 
// 2) Expose an endpoint that runs the checks
app.MapHealthChecks("/healthz");
 
app.Run();

That is it. Run the app and open https://localhost:5001/healthz in your browser. You will see the plain text word Healthy. You just built a working health check endpoint.

Right now it does not check anything real — it only proves the app is responding. The next step is to add checks that look at the parts your app depends on.

What happens when a request comes in

Here is the path a health request takes inside your app. The endpoint runs every registered check, gathers the results, picks the worst one, and writes a response.

How a health request is handled

Request
Run checks
Combine results
Pick worst
Write response

Steps

1

Request

GET /healthz arrives

2

Run checks

Each IHealthCheck runs

3

Combine results

Collect Healthy/Degraded/Unhealthy

4

Pick worst

Overall = lowest status

5

Write response

200 or 503 + body

The endpoint runs all checks, then reports the worst status it found.

The rule for the overall status is simple: the app is only as healthy as its weakest part. If one check is Unhealthy, the whole endpoint reports Unhealthy.

Adding a real check: the database

A health endpoint that checks nothing is not very useful. The most common real check is "can I reach my database?" For ready-made checks, the community Xabaril project gives you packages for almost every database and service.

For SQL Server, install the package:

dotnet add package AspNetCore.HealthChecks.SqlServer

Then register it. You can give each check a friendly name and tags. Tags are labels you use later to group checks.

builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("Default")!,
        name: "sql-server",
        tags: new[] { "db", "ready" })
    .AddRedis(
        redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
        name: "redis-cache",
        tags: new[] { "cache", "ready" });

Now when the robot calls /healthz, your app actually opens the database, runs a tiny query, and pings Redis. If the database is down, the endpoint reports Unhealthy and returns 503. This is the moment health checks start earning their keep.

The Xabaril project covers many systems. Here are a few popular packages.

What you want to checkNuGet packageTag idea
SQL ServerAspNetCore.HealthChecks.SqlServerdb
PostgreSQLAspNetCore.HealthChecks.Npgsqldb
RedisAspNetCore.HealthChecks.Rediscache
RabbitMQAspNetCore.HealthChecks.Rabbitmqbroker
A remote URLAspNetCore.HealthChecks.Urisexternal

Writing your own custom check

Sometimes you need to check something special — maybe a license file, a third-party API key, or a folder of incoming files. You can write your own check by making a class that implements IHealthCheck.

This check looks at how much free disk space is left and decides the status.

using Microsoft.Extensions.Diagnostics.HealthChecks;
 
public sealed class DiskSpaceHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var drive = new DriveInfo("C");
        var freeGb = drive.AvailableFreeSpace / 1_000_000_000d;
 
        if (freeGb < 1)
            return Task.FromResult(HealthCheckResult.Unhealthy(
                $"Only {freeGb:F1} GB free."));
 
        if (freeGb < 5)
            return Task.FromResult(HealthCheckResult.Degraded(
                $"Low disk: {freeGb:F1} GB free."));
 
        return Task.FromResult(HealthCheckResult.Healthy(
            $"{freeGb:F1} GB free."));
    }
}

Register it with a name and tag, just like the built-in checks.

builder.Services.AddHealthChecks()
    .AddCheck<DiskSpaceHealthCheck>(
        name: "disk-space",
        tags: new[] { "system", "live" });

The CheckHealthAsync method is where all the work happens. Keep it fast and light. A health check should answer in milliseconds, not run a heavy report. Remember the doctor: a quick pulse check, not a full body scan.

Liveness and readiness: the two important questions

This is the part that confuses most beginners, so let us slow down. There are two different questions a robot might ask:

  1. Liveness — "Are you alive?" Is the app process running and not stuck in a deadlock? If liveness fails, the right fix is to restart the app.
  2. Readiness — "Are you ready?" Can the app handle a real request right now? During startup it might still be warming caches or waiting for the database. If readiness fails, the right fix is to stop sending traffic for now, but do not restart.

An app can be alive but not ready. This happens all the time at startup.

Figure 2: The same app, two questions. Liveness controls restarts. Readiness controls traffic.

We use tags to split these. The liveness check should be tiny — just "is the process responding?" — and should not check the database. Why? If the database is down, you do not want Kubernetes to keep restarting your app forever. The app itself is fine; the database is the problem. That is a readiness concern.

Here is how to expose two endpoints using tags. The Predicate decides which checks run for each endpoint.

using Microsoft.Extensions.Diagnostics.HealthChecks;
 
// Liveness: run only checks tagged "live" (keep these trivial)
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});
 
// Readiness: run only checks tagged "ready" (db, cache, etc.)
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

Now /healthz/live answers the "are you alive?" question with cheap checks, and /healthz/ready answers "are you ready?" by testing the database and cache.

Connecting to Kubernetes

Kubernetes is the most common place health checks get used. It has three kinds of probes, and they map neatly onto what we built.

Kubernetes probeQuestionIf it failsMaps to
Startup probeHas the app finished starting?Wait longer before other probes/healthz/ready
Liveness probeIs the app alive?Restart the pod/healthz/live
Readiness probeCan it take traffic?Stop sending traffic/healthz/ready

This is what the probe section of a Kubernetes deployment looks like.

// This is YAML, shown here for reference inside the article.
// livenessProbe:
//   httpGet: { path: /healthz/live, port: 8080 }
//   initialDelaySeconds: 10
//   periodSeconds: 10
// readinessProbe:
//   httpGet: { path: /healthz/ready, port: 8080 }
//   initialDelaySeconds: 5
//   periodSeconds: 5

One very important safety note: Kubernetes probes do not send authentication headers. If you put an authorization requirement on /healthz/live or /healthz/ready, every probe will fail, and Kubernetes will kill your pod in a loop. Keep these simple probe endpoints open. Protect detailed diagnostic endpoints separately.

Kubernetes probe loop

Startup
Liveness
Readiness
Action

Steps

1

Startup

Wait until app is up

2

Liveness

Ping /healthz/live

3

Readiness

Ping /healthz/ready

4

Action

Restart or pause traffic

Each probe targets the matching endpoint and triggers the right action.

Returning a detailed JSON response

The plain text "Healthy" word is fine for robots, but humans often want detail: which check failed, and how long it took. You can pass a ResponseWriter to write JSON.

using System.Text.Json;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
 
app.MapHealthChecks("/healthz/details", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";
        var result = JsonSerializer.Serialize(new
        {
            status = report.Status.ToString(),
            totalDuration = report.TotalDuration.TotalMilliseconds,
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                durationMs = e.Value.Duration.TotalMilliseconds,
                description = e.Value.Description
            })
        });
        await context.Response.WriteAsync(result);
    }
});

This endpoint reveals names and timings, so treat it as sensitive. Protect it with authorization, or only expose it on an internal port that the public cannot reach.

A small dashboard with the Health Checks UI

If you like seeing a screen instead of reading JSON, the Xabaril project also offers a UI package. It shows a simple web page with green and red lights for each app you watch, and it can poll many services from one place.

You install AspNetCore.HealthChecks.UI together with a storage package (for example the in-memory one), point it at one or more health endpoints, and map the UI. It is handy for a team dashboard on a big screen. For most small projects, the JSON endpoint plus your existing monitoring tool is enough, so do not feel you must add it.

How the pieces fit together

Let us zoom out. You register checks once, then expose several endpoints, each filtered by tags for a different audience.

Figure 3: One set of checks, several endpoints, each for a different consumer.

This separation is the whole craft of health checks. The same underlying checks serve different needs because tags let each endpoint pick the right subset.

Common mistakes to avoid

A few traps catch almost everyone the first time. Keep this short list handy.

  • Checking the database in your liveness probe. If the DB goes down, Kubernetes restarts your app over and over, which never fixes the DB. Keep liveness trivial.
  • Slow checks. A health check that takes three seconds will make probes time out. Keep each check fast, and set a timeout on external calls.
  • Requiring auth on probe endpoints. Probes send no auth headers, so they will always fail. Keep probe endpoints open; protect detail endpoints.
  • Treating Degraded like Unhealthy. Degraded still returns 200 on purpose. Do not pull an app out of rotation just because a cache got a little slow.
  • No timeout on external URL checks. A frozen third-party API can freeze your health endpoint. Always cap the wait.

Putting it all together

Here is a compact, complete Program.cs that uses everything we covered: a liveness check, readiness checks for the database and cache, and a custom disk check.

var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(),
        tags: new[] { "live" })
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default")!,
        name: "sql-server", tags: new[] { "ready" })
    .AddRedis(
        builder.Configuration.GetConnectionString("Redis")!,
        name: "redis", tags: new[] { "ready" })
    .AddCheck<DiskSpaceHealthCheck>("disk",
        tags: new[] { "live" });
 
var app = builder.Build();
 
app.MapHealthChecks("/healthz/live", new()
{
    Predicate = c => c.Tags.Contains("live")
});
app.MapHealthChecks("/healthz/ready", new()
{
    Predicate = c => c.Tags.Contains("ready")
});
 
app.Run();

The "self" check always returns Healthy. It proves the process can respond to requests at all, which is the perfect, cheap liveness signal.

Quick recap

  • A health check is a quick check-up for your app. A monitor calls a URL and your app answers Healthy, Degraded, or Unhealthy.
  • The core API ships with ASP.NET Core. Register with AddHealthChecks() and expose with MapHealthChecks("/healthz").
  • Three states: Healthy (200), Degraded (200, still working), Unhealthy (503, stop traffic).
  • The community Xabaril packages give ready-made checks for SQL Server, PostgreSQL, Redis, RabbitMQ, and more.
  • Write your own check with IHealthCheck and keep CheckHealthAsync fast.
  • Split liveness (are you alive — controls restarts) from readiness (are you ready — controls traffic) using tags.
  • In Kubernetes, map liveness to /healthz/live and readiness to /healthz/ready. Keep probe endpoints open, since probes send no auth headers.
  • Use a ResponseWriter for detailed JSON, but protect that endpoint.

References and further reading

Related Posts