✓
ASP.NET Core 10

ASP.NET Core 10 Health Checks: Build Reliable Readiness and Liveness Probes

Advertisement
Ad space
.NET

A service can be running and still be unsafe to receive traffic. Its database may be unavailable, a startup task may still be warming a cache, or an essential remote dependency may be failing. ASP.NET Core health checks give load balancers, container platforms, and monitoring systems a clear HTTP signal about that state.

This guide shows how to build separate liveness and readiness probes in an ASP.NET Core 10 API. You will register tagged checks, expose focused endpoints, return useful JSON for operators, and avoid the common mistake of using one endpoint for every kind of failure.

Why a single health endpoint is often not enough

A simple /health endpoint is a good first step, but production systems usually need two different answers:

  • Liveness: Is the process alive enough that an orchestrator should keep it running? A failing liveness probe can trigger a restart.
  • Readiness: Can this instance safely receive application traffic? A failing readiness probe should usually remove the instance from a load balancer without necessarily restarting it.

Those meanings matter. If a database has a short outage, restarting every API process may create extra load and make recovery harder. A readiness failure is often the right signal: keep the process alive, stop routing new requests to it, and let normal retry or recovery logic work.

Start with the built-in health-check services

ASP.NET Core includes health-check middleware. Register it with AddHealthChecks, then expose endpoints with MapHealthChecks. No third-party package is needed for the core routing and check abstractions.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/health/live");

app.Run();

At this point, the endpoint confirms that the application can answer the health-check request. It does not verify a database, cache, queue, or third-party API. That is useful for a liveness signal, but it is not a complete readiness check.

Tag checks so each endpoint has a clear purpose

Tags let you decide which checks run for each endpoint. In this example, the application has a small custom startup check and a readiness endpoint that includes all checks tagged ready.

using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
    .AddCheck<StartupHealthCheck>(
        "startup",
        tags: new[] { "ready" });

var app = builder.Build();

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

app.Run();

The liveness endpoint deliberately excludes dependency checks by returning false from its predicate. That means it answers the narrow question “is the web process alive?” The readiness endpoint runs the checks tagged ready.

Create a startup readiness check

Some applications load reference data, download configuration, or establish a connection before they should accept traffic. The following check starts as unhealthy and becomes healthy when the startup work completes.

using Microsoft.Extensions.Diagnostics.HealthChecks;

public sealed class StartupHealthCheck : IHealthCheck
{
    private volatile bool _startupCompleted;

    public void MarkStartupCompleted()
    {
        _startupCompleted = true;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult(_startupCompleted
            ? HealthCheckResult.Healthy("Startup work completed.")
            : HealthCheckResult.Unhealthy("Startup work is still running."));
    }
}

Register this class as the check implementation, then let the component that owns startup work mark it complete. For example, a hosted service can receive the same singleton instance and call MarkStartupCompleted after it finishes.

Add a dependency check carefully

Health checks can verify dependencies such as a database or an external service. Only add a dependency to readiness when the application cannot safely handle requests without it. A best-effort analytics endpoint might degrade gracefully when a reporting API is down; a transactional endpoint backed by a database usually cannot.

For an EF Core application, add the supported EF Core health-check package and register a DbContext check. Keep the probe inexpensive: it runs repeatedly in production.

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

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>(
        name: "database",
        tags: new[] { "ready" });

A database check confirms connectivity, not that every application query or migration is valid. Monitor query latency, errors, and saturation separately.

Return JSON for internal diagnostics

The default plaintext response is enough for many probes. An internal operations endpoint can benefit from structured output, but do not expose database names, connection strings, exception messages, or provider-specific details to the public internet.

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";

        var payload = new
        {
            status = report.Status.ToString(),
            duration = report.TotalDuration.TotalMilliseconds,
            checks = report.Entries.Select(entry => new
            {
                name = entry.Key,
                status = entry.Value.Status.ToString(),
                duration = entry.Value.Duration.TotalMilliseconds
            })
        };

        await context.Response.WriteAsJsonAsync(payload);
    }
});

Consider protecting a detailed management endpoint through a private network, a management port, or authorization. Your public liveness probe should stay small and reveal as little implementation detail as possible.

Use the endpoints with Kubernetes

Container orchestration is where the distinction becomes especially useful. Point the readiness probe at /health/ready and the liveness probe at /health/live. Tune delays and failure thresholds using your real startup time and expected recovery behavior.

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 15

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3

Do not copy probe timings blindly. A service that initializes in two seconds has very different needs from one that warms a large cache or waits for a migration job. Measure startup under realistic resource limits before deciding the values.

Common health-check mistakes

  • Using a database call as liveness: a temporary database failure should not automatically mean the process needs a restart.
  • Making checks expensive: probes run frequently; avoid large queries, full cache scans, and calls that create load spikes.
  • Returning secret details: expose status names and durations, not credentials or raw exception content.
  • Checking every optional dependency: only fail readiness for dependencies needed to serve the traffic you intend to accept.
  • Skipping monitoring: health checks are a signal, not a replacement for logs, traces, metrics, and alerting.

Production checklist

  • Expose separate liveness and readiness endpoints.
  • Tag each dependency check by purpose.
  • Keep liveness independent of transient dependencies.
  • Make readiness checks fast and safe to run repeatedly.
  • Protect diagnostic detail from public access.
  • Test what your load balancer or orchestrator actually does after each failure.
  • Track probe failures alongside application errors and latency.

Final thoughts

Health checks work best when they communicate one clear operational decision. Liveness asks whether the process should stay alive; readiness asks whether it should receive traffic. Separating those concerns gives Kubernetes, load balancers, and on-call engineers better information—and helps your ASP.NET Core API recover more predictably.

For the complete API surface and additional examples, review Microsoft’s ASP.NET Core health-check documentation.

Related CodeWithChai resources

Sponsored
Ad space
🎉 Finished reading. Nice work!