Skip to main content

ASP.NET Core Health Checks: Complete Guide with Examples

Learn ASP.NET Core health checks step by step — monitor databases, APIs, and memory in production with real C# examples. Start monitoring like a pro today.

When your production app goes down at 2 AM, how quickly will you know? If your answer is "when a customer emails us," you need ASP.NET Core health checks. Health checks are lightweight endpoints built into ASP.NET Core that report whether your application — and everything it depends on, like SQL Server, Redis, or third-party APIs — is actually working. Load balancers, Kubernetes, and monitoring dashboards poll these endpoints continuously, so failures are detected and handled in seconds, not hours.

In this guide, you'll learn how to add a health check endpoint in ASP.NET Core, monitor databases and external services, distinguish liveness from readiness probes, secure your endpoints, and visualize everything with a dashboard. Every example is runnable on .NET 8 and .NET 9.

What Are Health Checks in ASP.NET Core?

A health check is a small piece of code that answers one question: is this part of my system healthy right now? ASP.NET Core ships with a first-class health checks framework in the Microsoft.Extensions.Diagnostics.HealthChecks package (included in the shared framework, so no extra install is needed for the basics).

Each check returns one of three states:

  • Healthy — the component is working normally.
  • Degraded — it works, but slowly or partially (e.g., a query took 5 seconds instead of 50 ms). The app can still serve traffic.
  • Unhealthy — the component is broken. Orchestrators may restart the app or pull it out of the load balancer rotation.

Why does this matter? Because modern apps rarely fail all at once. Your web server might be fine while your database connection pool is exhausted. Without health checks, that failure surfaces as mysterious 500 errors sprinkled across user requests. With health checks, your infrastructure sees "database: unhealthy" and can react automatically — restart the pod, fail over, alert the on-call engineer — before most users notice.

Adding Your First Health Check Endpoint

The simplest possible setup takes two lines in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();

Run the app and browse to https://localhost:5001/health. You'll see a plain-text response: Healthy, with HTTP status 200. If any registered check fails, the endpoint returns 503 Service Unavailable — exactly what load balancers look for.

This "empty" check is already useful: it proves the ASP.NET Core pipeline is up and processing requests. But the real power comes from checking your dependencies.

Checking Databases, Redis, and External APIs

The community-maintained AspNetCore.Diagnostics.HealthChecks project provides ready-made checks for virtually every dependency you'll ever use. Install the ones you need:

// Package references:
// dotnet add package AspNetCore.HealthChecks.SqlServer
// dotnet add package AspNetCore.HealthChecks.Redis
// dotnet add package AspNetCore.HealthChecks.Uris

builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("Default")!,
        name: "sql-server",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "ready", "database" })
    .AddRedis(
        redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
        name: "redis-cache",
        failureStatus: HealthStatus.Degraded,   // cache down = slower, not broken
        tags: new[] { "ready", "cache" })
    .AddUrlGroup(
        new Uri("https://api.payment-provider.com/status"),
        name: "payment-api",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "ready", "external" });

Notice two deliberate decisions here, because this is where the why matters:

  • Redis failing is marked Degraded, not Unhealthy. If your app can fall back to the database when the cache is down, it shouldn't be killed by Kubernetes just because Redis hiccupped. Map failure status to actual business impact.
  • Tags group checks by purpose. We'll use the "ready" tag in a moment to build separate liveness and readiness endpoints — one of the most important patterns in ASP.NET Core production monitoring.

Writing a Custom Health Check in C#

When no prebuilt check fits — say you need to verify disk space or a message queue backlog — implement IHealthCheck:

public class MemoryHealthCheck : IHealthCheck
{
    private const long ThresholdBytes = 1_024L * 1_024L * 1_024L; // 1 GB

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var allocated = GC.GetTotalMemory(forceFullCollection: false);

        var data = new Dictionary<string, object>
        {
            ["AllocatedBytes"] = allocated,
            ["Gen0Collections"] = GC.CollectionCount(0),
            ["Gen2Collections"] = GC.CollectionCount(2)
        };

        var status = allocated < ThresholdBytes
            ? HealthCheckResult.Healthy("Memory usage is normal.", data)
            : HealthCheckResult.Degraded(
                $"High memory usage: {allocated / 1_048_576} MB allocated.", data: data);

        return Task.FromResult(status);
    }
}

Register it with a timeout so a hung check can't stall the whole endpoint:

builder.Services.AddHealthChecks()
    .AddCheck<MemoryHealthCheck>(
        "memory",
        tags: new[] { "live" },
        timeout: TimeSpan.FromSeconds(3));

The data dictionary rides along in the JSON response (shown next), giving your monitoring system real numbers to graph and alert on — not just a binary up/down.

Liveness vs. Readiness: The Pattern That Saves Production

This is the single most important concept in this article. Kubernetes, Azure App Service, and AWS load balancers ask two different questions:

  • Liveness — "Is the process alive, or should I restart it?" This should only check the app itself. If your liveness probe includes the database and the database goes down, Kubernetes will restart every pod in a loop — making the outage worse, not better.
  • Readiness — "Can this instance serve traffic right now?" This is where dependency checks belong. An instance that can't reach SQL Server should be pulled from rotation, but not killed.

Tags make this trivial to express:

// Liveness: cheap, no dependencies. Answers "is the process running?"
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});

// Readiness: full dependency check. Answers "can I take traffic?"
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

In a Kubernetes deployment, wire them up like this:

// deployment.yaml (excerpt)
// livenessProbe:
//   httpGet: { path: /health/live, port: 8080 }
//   periodSeconds: 10
//   failureThreshold: 3
// readinessProbe:
//   httpGet: { path: /health/ready, port: 8080 }
//   periodSeconds: 5

Returning Detailed JSON Responses

The default plain-text "Healthy" is fine for machines but useless for humans debugging an incident. The ResponseWriter option lets you emit rich JSON:

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

        var result = JsonSerializer.Serialize(new
        {
            status = report.Status.ToString(),
            totalDurationMs = 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,
                data = e.Value.Data
            })
        }, new JsonSerializerOptions { WriteIndented = true });

        await context.Response.WriteAsync(result);
    }
});

Now a failing readiness probe tells you exactly which dependency failed and how long each check took — the difference between a 5-minute fix and an hour of guessing.

Health Checks UI: A Free Monitoring Dashboard

The AspNetCore.HealthChecks.UI package gives you a polling dashboard with history and webhook alerts (Slack, Teams, email) — no third-party APM subscription required:

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

builder.Services.AddHealthChecksUI(options =>
{
    options.SetEvaluationTimeInSeconds(30);      // poll every 30s
    options.SetMinimumSecondsBetweenFailureNotifications(300);
    options.AddHealthCheckEndpoint("My API", "/health/ready");
}).AddInMemoryStorage();

// ...

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

Browse to /health-ui and you get a live dashboard showing every check, its status, and its failure history. For teams that already run Prometheus or Datadog, the JSON endpoint above integrates directly instead.

Best Practices and Common Pitfalls

Best practices

  • Keep liveness checks dependency-free. A liveness failure means "restart me." Only report unhealthy for problems a restart can actually fix, such as a deadlocked thread pool.
  • Set timeouts on every check. A health check that hangs for 30 seconds waiting on a dead database will cause probe timeouts and cascading restarts. Two to five seconds is a sensible ceiling.
  • Use Degraded deliberately. Reserve Unhealthy for "cannot serve users." Cache misses, slow-but-working queries, and optional integrations belong in Degraded so dashboards show trouble without triggering restarts.
  • Secure detailed endpoints. Your JSON response leaks infrastructure details (server names, dependency lists). Expose /health/live publicly for probes, but protect the detailed endpoint: app.MapHealthChecks("/health/detail", options).RequireAuthorization("OpsPolicy"); or restrict it to an internal port with RequireHost("*:5001").
  • Make checks cheap. Probes fire every few seconds across every instance. A SQL check should run SELECT 1, not a full table scan. If a check is inherently expensive, cache its result for 10–30 seconds inside the check itself.

Common pitfalls

  • Putting the database in the liveness probe. The classic mistake: database blips, Kubernetes restarts all pods simultaneously, and a 30-second outage becomes a 10-minute one.
  • Health-checking dependencies you don't own the fix for. If a third-party API is down and your app can queue work and retry, reporting Unhealthy takes your app down over their outage.
  • Forgetting that checks run on the request thread. Blocking calls (.Result, .Wait()) inside CheckHealthAsync can starve the thread pool under load — always use async I/O.
  • One giant /health endpoint for everything. Different consumers need different answers. Load balancers, orchestrators, and humans should hit different endpoints filtered by tags.

Conclusion: Monitor Like a Pro with ASP.NET Core Health Checks

ASP.NET Core health checks turn "we found out from a customer" into "the load balancer routed around it automatically." They're built into the framework, cost almost nothing to add, and pay for themselves during the very first incident.

Key takeaways:

  • Start with AddHealthChecks() and MapHealthChecks("/health") — two lines gets you a working endpoint.
  • Add prebuilt checks for SQL Server, Redis, and external APIs; write IHealthCheck implementations for anything custom.
  • Split liveness (process alive, no dependencies) from readiness (can serve traffic, all dependencies) using tags — this one pattern prevents restart storms.
  • Map failure status to business impact: Unhealthy means "pull me out of rotation," Degraded means "watch me."
  • Emit detailed JSON for humans, secure it behind authorization, and use the free Health Checks UI dashboard for at-a-glance monitoring.

Add a health check endpoint to your ASP.NET Core app today — your future 2 AM self will thank you.

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

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...