Health Checks

Expose liveness and readiness endpoints with AddHealthChecks, and understand why the two must never check the same things.

Syntaxapp.MapHealthChecks("/healthz/ready", new HealthCheckOptions { Predicate = r => r.Tags.Contains("ready") });

A health check is an endpoint that answers one question about your service with an HTTP status code. Something outside the service calls it on a schedule and acts on the answer. Kubernetes, the system that runs containers in production, calls such an endpoint a probe.

Two different questions

A liveness check asks: is this process still working at all? When it fails, the container is killed and a fresh one is started. A readiness check asks: can this instance serve traffic right now? When it fails, the instance is taken out of the load balancer but left running.

They must check different things. If a liveness check queries the database, one short database outage fails every instance at once, and every instance is restarted. Restarting does not fix a database, and now you have no service either. Dependency checks belong in readiness. Keep liveness trivial.

How it is wired

AddHealthChecks() registers the services. AddCheck<T> adds one named check, with optional tags, which are just labels you can filter on. MapHealthChecks creates the endpoint, and its Predicate decides which registered checks run there. A check returns HealthCheckResult.Healthy, Degraded or Unhealthy. By default healthy and degraded both answer 200, and unhealthy answers 503.

Shorthand

tags: ["ready"] is a collection expression, added in C# 12. It compiles to the same thing as tags: new[] { "ready" }. Both are fine to read and write.

What trips people up

A check with no time limit hangs the probe instead of failing it, and a hung probe looks like a healthy one for as long as the orchestrator waits. Give every dependency check a timeout.

Example

Example · csharp
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

// Register the health check services, then add one check labelled "ready".
builder.Services.AddHealthChecks()
    .AddCheck<DatabaseCheck>("database", tags: ["ready"]);

var app = builder.Build();

// Liveness. Predicate selects no checks, so this only proves the process
// is still accepting and answering requests.
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = _ => false
});

// Readiness. Only checks tagged "ready" run here.
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = registration => registration.Tags.Contains("ready")
});

app.MapGet("/", () => "ok");
app.Run();

// A check is any class implementing IHealthCheck.
sealed class DatabaseCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        // Stands in for the real round trip, such as "SELECT 1".
        bool reachable = true;

        return Task.FromResult(reachable
            ? HealthCheckResult.Healthy("database reachable")
            : HealthCheckResult.Unhealthy("database unreachable"));
    }
}

When to use it

  • Let Kubernetes restart a container whose request loop has deadlocked, without restarting it for a database blip.
  • Keep a newly started instance out of the load balancer until its caches are warm.
  • Give a load balancer one URL that says whether this instance should receive traffic.
  • Fail readiness during shutdown so traffic drains before the process exits.

More examples

A dependency check with a time limit

AddCheck also takes a timeout: AddCheck<DatabaseCheck>("database", tags: ["ready"], timeout: TimeSpan.FromSeconds(2)) does the same job. Add with a HealthCheckRegistration is the fuller form, used when you want to hand over an instance or a factory delegate you built yourself. Either way, after two seconds the check is abandoned and reported as the failureStatus you chose. Reach for a timeout whenever a check talks to something over a network, which is the only kind of check that can hang.

Example · csharp
using Microsoft.Extensions.Diagnostics.HealthChecks;

builder.Services.AddHealthChecks()
    .Add(new HealthCheckRegistration(
        name: "database",
        instance: new DatabaseCheck(),
        failureStatus: HealthStatus.Unhealthy,
        tags: ["ready"],
        timeout: TimeSpan.FromSeconds(2)));

Checking an EF Core database without writing the check

AddDbContextCheck asks your DbContext whether it can reach its database, so you do not hand-write the query. DisableHttpMetrics, available since .NET 9, stops probe requests being counted in http.server.request.duration; a probe every five seconds otherwise dominates your latency chart because it is fast and constant.

Example · csharp
// Package: Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>(name: "database", tags: ["ready"]);

var app = builder.Build();

// Health probe traffic would otherwise distort your request metrics.
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = registration => registration.Tags.Contains("ready")
}).DisableHttpMetrics();

The two endpoints as Kubernetes probes

This is the container side of the same arrangement. The startup probe gives a slow-starting app up to ninety seconds to come up without the liveness probe killing it first; liveness only begins once the startup probe has passed once. failureThreshold is how many consecutive failures count as a real failure, which is what stops one dropped packet from restarting a container.

Example · yaml
livenessProbe:
  httpGet:
    path: /healthz/live
    port: 8080
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /healthz/ready
    port: 8080
  periodSeconds: 5
  failureThreshold: 2
startupProbe:
  httpGet:
    path: /healthz/live
    port: 8080
  periodSeconds: 3
  failureThreshold: 30

Discussion

  • Be the first to comment on this lesson.