Skip to main content

Health Checks in ASP.NET Core: Complete Guide 2026

Learn how to add health checks in ASP.NET Core to monitor your production app. Step-by-step tutorial with code, best practices, and Kubernetes probes. Start now!

If your production API goes down at 3 a.m., how do you find out first — from your monitoring system, or from an angry customer? Health checks in ASP.NET Core are the built-in feature that lets your monitoring tools, load balancers, and orchestrators know whether your app is alive, ready to serve traffic, and able to reach its dependencies. In this tutorial you'll learn how to add health checks in ASP.NET Core from scratch, wire up readiness and liveness probes for Kubernetes, check databases and external services, and follow the best practices seniors use in real production systems.

Whether you're a beginner searching for "how to add a health check endpoint" or a senior engineer looking for advanced health check patterns in C#, this guide covers it all with runnable code for .NET 8 and .NET 9.

What Are Health Checks in ASP.NET Core?

A health check is a lightweight endpoint your application exposes (typically /healthz or /health) that reports whether the app — and the services it depends on — are working correctly. Instead of guessing, an external system makes an HTTP request and reads a simple status:

  • Healthy — everything works, keep sending traffic.
  • Degraded — working but slow or partially impaired (e.g., a cache is down but the DB is fine).
  • Unhealthy — something critical is broken, stop routing traffic here.

Why does this matter? Modern infrastructure is built to react to these signals automatically. A Kubernetes cluster restarts a pod that fails its liveness probe. An Azure or AWS load balancer removes an unhealthy instance from rotation. Your Datadog, Prometheus, or Grafana dashboard alerts on-call engineers. Without health checks, all of that automation is blind.

How to Add Health Checks in ASP.NET Core (Quick Start)

The health checks feature is built into ASP.NET Core — no NuGet package needed for the basics. Registering it takes exactly two lines. In your Program.cs:

var builder = WebApplication.CreateBuilder(args);

// 1. Register the health checks services
builder.Services.AddHealthChecks();

var app = builder.Build();

// 2. Map the health check endpoint
app.MapHealthChecks("/healthz");

app.Run();

That's it. Run the app and hit https://localhost:5001/healthz. You'll get a 200 OK with the body Healthy. This confirms the process is running and the ASP.NET Core pipeline can handle requests — a basic liveness signal.

But a bare "the process is up" check is rarely enough. Your app is only truly healthy if it can reach its database, message broker, and critical downstream APIs. That's where custom checks come in.

Adding Database and Dependency Health Checks

The community-maintained AspNetCore.Diagnostics.HealthChecks project provides ready-made checks for almost every dependency: SQL Server, PostgreSQL, MySQL, Redis, RabbitMQ, Azure Blob Storage, MongoDB, and dozens more. Install the package you need:

// dotnet add package AspNetCore.HealthChecks.SqlServer
// dotnet add package AspNetCore.HealthChecks.Redis

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

Notice the tags parameter — this is one of the most important features for real-world setups. Tags let you group checks so you can expose different endpoints for different purposes, which we'll use for Kubernetes probes below.

Writing a Custom Health Check

When you need to check something specific — a third-party payment gateway, a licensing service, or a business rule like "is the nightly import queue backed up?" — implement the IHealthCheck interface:

public sealed class PaymentGatewayHealthCheck : IHealthCheck
{
    private readonly IHttpClientFactory _httpClientFactory;

    public PaymentGatewayHealthCheck(IHttpClientFactory httpClientFactory)
        => _httpClientFactory = httpClientFactory;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var client = _httpClientFactory.CreateClient("payments");
            using var response = await client.GetAsync("/ping", cancellationToken);

            if (response.IsSuccessStatusCode)
                return HealthCheckResult.Healthy("Payment gateway reachable.");

            // Degraded: app still works, but with reduced capability
            return HealthCheckResult.Degraded(
                $"Gateway returned {(int)response.StatusCode}.");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Payment gateway unreachable.", exception: ex);
        }
    }
}

Register it with a name and tags:

builder.Services.AddHealthChecks()
    .AddCheck<PaymentGatewayHealthCheck>(
        "payment-gateway",
        tags: new[] { "ready", "external" });

Why return Degraded instead of Unhealthy? This is a key design decision. If your app can still serve most requests when a non-critical dependency is down, returning Degraded keeps the instance in the load balancer rotation while still surfacing the problem on your dashboards. Marking it Unhealthy would pull the whole instance out of service — often an overreaction that turns a minor issue into an outage.

Readiness and Liveness Probes for Kubernetes

This is where health checks in ASP.NET Core become essential for cloud-native deployments. Kubernetes uses two distinct probes, and confusing them is one of the most common production mistakes:

  • Liveness probe — "Is the process alive?" If this fails, Kubernetes restarts the pod. It should be cheap and must not check external dependencies. Otherwise, when your database has a hiccup, Kubernetes will restart every single pod in a pointless crash loop.
  • Readiness probe — "Is this pod ready to receive traffic?" If this fails, Kubernetes stops sending traffic but leaves the pod running. This one should check dependencies like the database.

Use tags to expose two separate endpoints from the same app:

using Microsoft.AspNetCore.Diagnostics.HealthChecks;

// Readiness: run ALL checks tagged "ready" (DB, cache, external services)
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

// Liveness: run NO dependency checks — just confirm the app responds
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = _ => false
});

The corresponding Kubernetes deployment manifest ties it together:

// deployment.yaml (excerpt)
// livenessProbe:
//   httpGet:
//     path: /healthz/live
//     port: 8080
//   initialDelaySeconds: 10
//   periodSeconds: 15
// readinessProbe:
//   httpGet:
//     path: /healthz/ready
//     port: 8080
//   initialDelaySeconds: 5
//   periodSeconds: 10

Now a temporary database outage takes pods out of rotation (readiness fails) without triggering restart storms (liveness still passes). When the database recovers, pods automatically rejoin the load balancer. This is exactly how you "monitor your production app like a pro."

Returning Detailed JSON Health Responses

The default plain-text response is fine for probes, but your monitoring tools and human engineers want detail: which check failed, how long it took, and why. Customize the response writer to emit JSON:

using System.Text.Json;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;

app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";

        var payload = new
        {
            status = report.Status.ToString(),
            totalDurationMs = report.TotalDuration.TotalMilliseconds,
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                durationMs = e.Value.Duration.TotalMilliseconds,
                error = e.Value.Exception?.Message
            })
        };

        await context.Response.WriteAsync(
            JsonSerializer.Serialize(payload));
    }
});

A sample response now looks like this — perfect for a dashboard or an alert payload:

// {
//   "status": "Degraded",
//   "totalDurationMs": 42.7,
//   "checks": [
//     { "name": "sql-server", "status": "Healthy", "durationMs": 12.1 },
//     { "name": "redis-cache", "status": "Degraded", "description": "Timeout" },
//     { "name": "payment-gateway", "status": "Healthy", "durationMs": 30.4 }
//   ]
// }

Bonus: The Health Checks UI Dashboard

For a visual dashboard, add the AspNetCore.HealthChecks.UI package. It polls your health endpoints and renders a live status page — great for ops teams and demos:

// dotnet add package AspNetCore.HealthChecks.UI
// dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage

builder.Services
    .AddHealthChecksUI(opt =>
    {
        opt.SetEvaluationTimeInSeconds(15);
        opt.AddHealthCheckEndpoint("API", "/healthz");
    })
    .AddInMemoryStorage();

app.MapHealthChecksUI(opt => opt.UIPath = "/health-ui");

Health Check Best Practices and Common Pitfalls

Following ASP.NET Core health check best practices separates a robust production system from a fragile one. Keep these in mind:

  • Never make health checks expensive. A check that runs a heavy query can hammer your database when the probe fires every 10 seconds across dozens of pods. Use lightweight pings (SELECT 1) and short timeouts.
  • Keep liveness free of dependencies. The single most common mistake: putting a database check in the liveness probe. A DB blip then restarts every pod, amplifying the outage instead of containing it.
  • Set timeouts on every dependency check. A hung external call should fail fast, not block the whole health response. Wrap checks with a CancellationToken deadline.
  • Secure or hide detailed endpoints. Detailed JSON reveals your infrastructure (which databases, caches, and services you use). Expose the detailed endpoint only internally, or protect it with authorization.
  • Cache results under load. Consider the Microsoft.Extensions.Diagnostics.HealthChecks publisher or a caching layer so a flood of probes doesn't create a self-inflicted denial of service.
  • Use Degraded deliberately. Reserve Unhealthy for problems that genuinely mean "stop sending traffic." Non-critical dependencies should degrade, not fail the whole instance.
  • Return the right HTTP status. By default, Healthy and Degraded both return 200, and Unhealthy returns 503. Configure ResultStatusCodes if your load balancer expects different behavior.

Advanced: Health Check Publishers for Push-Based Monitoring

Everything above is pull-based — something calls your endpoint. For senior-level setups, ASP.NET Core also supports push-based monitoring via IHealthCheckPublisher. The framework runs your checks on a timer and pushes results to any sink you choose (Application Insights, Prometheus pushgateway, a Slack webhook):

public sealed class LoggingHealthCheckPublisher : IHealthCheckPublisher
{
    private readonly ILogger<LoggingHealthCheckPublisher> _logger;

    public LoggingHealthCheckPublisher(ILogger<LoggingHealthCheckPublisher> logger)
        => _logger = logger;

    public Task PublishAsync(HealthReport report, CancellationToken ct)
    {
        if (report.Status != HealthStatus.Healthy)
            _logger.LogWarning("Health degraded: {Status}", report.Status);

        return Task.CompletedTask;
    }
}

// Register the publisher and its schedule
builder.Services.AddSingleton<IHealthCheckPublisher, LoggingHealthCheckPublisher>();
builder.Services.Configure<HealthCheckPublisherOptions>(opt =>
{
    opt.Delay = TimeSpan.FromSeconds(5);
    opt.Period = TimeSpan.FromSeconds(30);
});

This decouples monitoring from inbound requests entirely — your app proactively reports its own health even when no probe is calling it.

Key Takeaways

Implementing health checks in ASP.NET Core is one of the highest-value, lowest-effort things you can do to run a reliable production application. Here's what to remember:

  • Start with AddHealthChecks() and MapHealthChecks() — two lines to a working endpoint.
  • Add dependency checks (SQL Server, Redis, external APIs) and custom IHealthCheck implementations for business-critical services.
  • Split liveness and readiness probes using tags — never check dependencies in liveness, or you'll cause restart storms.
  • Return detailed JSON for dashboards, but secure it since it exposes your infrastructure.
  • Follow best practices: keep checks cheap, set timeouts, cache under load, and use Degraded deliberately.
  • For advanced push-based monitoring, use IHealthCheckPublisher.

With these patterns in place, your infrastructure can detect, isolate, and recover from failures automatically — so you find out about problems before your users do. Add health checks in ASP.NET Core to your next deployment and start monitoring your production app like a pro.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments