Skip to main content

ASP.NET Core Health Checks: Complete Guide (2026)

Learn how to implement ASP.NET Core health checks to monitor databases, APIs & services. Step-by-step tutorial with code examples. Start monitoring now!

ASP.NET Core Health Checks: Monitor Your Production App Like a Pro

Your app is deployed. Users are active. Then at 2 AM, the database silently dies and nobody knows until customers start tweeting. Sound familiar? ASP.NET Core health checks solve this exact problem by giving you real-time visibility into the state of your application and its dependencies — databases, external APIs, message queues, disk storage, and more.

In this guide, you'll learn how to implement health checks in ASP.NET Core from scratch, build custom health check logic, integrate with Kubernetes probes, and follow production-tested best practices that keep your systems observable and reliable.

What Are Health Checks in ASP.NET Core?

Health checks are HTTP endpoints that report whether your application and its dependencies are functioning correctly. ASP.NET Core ships with built-in health check middleware (the Microsoft.Extensions.Diagnostics.HealthChecks package) that makes it straightforward to expose /health endpoints returning one of three statuses:

  • Healthy — everything is working as expected.
  • Degraded — the app is running but something is slower or partially broken.
  • Unhealthy — a critical dependency has failed.

Load balancers, container orchestrators like Kubernetes, and monitoring tools such as Prometheus and Grafana poll these endpoints to make automated decisions — restarting containers, pulling instances from rotation, or firing alerts.

Setting Up a Basic Health Check Endpoint

ASP.NET Core 6+ includes health check support out of the box. No extra NuGet packages are needed for the basics.

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register health check services
builder.Services.AddHealthChecks();

var app = builder.Build();

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

app.Run();

Run your application and hit GET /health. You'll receive a 200 OK response with the body Healthy. That's the simplest possible health check — it confirms the app process is alive and can handle HTTP requests.

But a process being alive doesn't mean your app is working. Let's add real dependency checks.

Adding Database Health Checks

Most production issues trace back to database connectivity. The AspNetCore.HealthChecks.SqlServer NuGet package (part of the popular Xabaril health checks library) provides ready-made checks for SQL Server, PostgreSQL, MySQL, and more.

// Install: dotnet add package AspNetCore.HealthChecks.SqlServer

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        healthQuery: "SELECT 1;",
        name: "sql-server",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "sql", "critical" });

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();

This check opens a connection to SQL Server and runs SELECT 1. If the connection times out or the query fails, the endpoint returns Unhealthy. The tags parameter becomes important later when you want to run different checks for different scenarios.

Entity Framework Core Health Check

If you're using EF Core, there's a built-in package that verifies your DbContext can connect:

// Install: dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore

builder.Services.AddHealthChecks()
    .AddDbContextCheck<ApplicationDbContext>(
        name: "ef-core-db",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "ef-core" });

This calls DbContext.Database.CanConnectAsync() under the hood — simple, but effective for catching connection string misconfigurations, network partitions, or database server outages.

Writing a Custom Health Check in ASP.NET Core

Pre-built checks cover common scenarios, but your application has unique dependencies — a third-party payment API, a file share, a Redis cache, a machine learning inference endpoint. Writing a custom health check in C# means implementing the IHealthCheck interface:

using Microsoft.Extensions.Diagnostics.HealthChecks;

public class PaymentGatewayHealthCheck : IHealthCheck
{
    private readonly HttpClient _httpClient;

    public PaymentGatewayHealthCheck(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient("PaymentGateway");
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var response = await _httpClient.GetAsync("/api/status", cancellationToken);

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

            return HealthCheckResult.Degraded(
                $"Payment gateway returned {response.StatusCode}.");
        }
        catch (HttpRequestException ex)
        {
            return HealthCheckResult.Unhealthy(
                "Payment gateway is unreachable.", ex);
        }
    }
}

Register it in Program.cs:

builder.Services.AddHttpClient("PaymentGateway", client =>
{
    client.BaseAddress = new Uri("https://api.paymentprovider.com");
    client.Timeout = TimeSpan.FromSeconds(5);
});

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

Notice we set failureStatus to Degraded rather than Unhealthy. This is a design decision: if the payment gateway is down, users can still browse products — the app isn't completely broken. Reserve Unhealthy for dependencies whose failure means the application cannot serve its core purpose.

Returning Detailed JSON Health Check Responses

The default health check endpoint returns a plain text status. In production, you want structured data — which checks passed, which failed, and how long each took. Use a custom ResponseWriter:

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

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

        var result = new
        {
            status = report.Status.ToString(),
            totalDuration = report.TotalDuration.TotalMilliseconds,
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                duration = e.Value.Duration.TotalMilliseconds,
                exception = e.Value.Exception?.Message,
                data = e.Value.Data
            })
        };

        var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
        {
            WriteIndented = true
        });

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

Now GET /health returns something like this:

// Sample JSON response
{
  "status": "Degraded",
  "totalDuration": 245.12,
  "checks": [
    {
      "name": "sql-server",
      "status": "Healthy",
      "description": null,
      "duration": 12.4,
      "exception": null,
      "data": {}
    },
    {
      "name": "payment-gateway",
      "status": "Degraded",
      "description": "Payment gateway returned ServiceUnavailable.",
      "duration": 232.7,
      "exception": null,
      "data": {}
    }
  ]
}

ASP.NET Core Readiness and Liveness Probes for Kubernetes

If you're deploying to Kubernetes (or Docker Swarm, or Azure Container Apps), you need separate endpoints for liveness and readiness probes. This is where health check tags shine:

  • Liveness — Is the process alive? If not, Kubernetes kills and restarts the pod.
  • Readiness — Can the app handle traffic? If not, Kubernetes stops routing requests to it but keeps the pod running.
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "sql-server",
        tags: new[] { "ready", "db" })
    .AddCheck<PaymentGatewayHealthCheck>(
        "payment-gateway",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "ready", "external" });

// Liveness: only checks tagged "live"
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});

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

In your Kubernetes deployment manifest:

// Kubernetes YAML (shown here for context)
// livenessProbe:
//   httpGet:
//     path: /health/live
//     port: 8080
//   initialDelaySeconds: 5
//   periodSeconds: 10
//
// readinessProbe:
//   httpGet:
//     path: /health/ready
//     port: 8080
//   initialDelaySeconds: 10
//   periodSeconds: 15

This separation prevents Kubernetes from restarting your pod just because a downstream API is temporarily unavailable. The pod stays alive (liveness passes) but is removed from the service load balancer (readiness fails) until the dependency recovers.

Health Check UI: Visual Dashboard

For teams that want a visual dashboard without building one from scratch, the AspNetCore.HealthChecks.UI package provides a ready-made web interface:

// Install packages:
// dotnet add package AspNetCore.HealthChecks.UI
// dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage

builder.Services.AddHealthChecksUI(options =>
{
    options.SetEvaluationTimeInSeconds(30);
    options.MaximumHistoryEntriesPerEndpoint(50);
    options.AddHealthCheckEndpoint("API", "/health");
})
.AddInMemoryStorage();

var app = builder.Build();

app.MapHealthChecks("/health", new HealthCheckOptions
{
    Predicate = _ => true,
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

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

Navigate to /health-ui and you get a real-time dashboard showing every registered check, its status history, and response times. It's an excellent tool for dev and staging environments. For production, you'll likely push health data to Prometheus, Datadog, or Application Insights instead.

Securing Your Health Check Endpoints

Health check endpoints can leak sensitive information — database connection status, internal service names, error messages. Never expose detailed health checks publicly without protection:

// Option 1: Require authorization
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{
    ResponseWriter = WriteDetailedResponse
}).RequireAuthorization("HealthCheckPolicy");

// Option 2: Restrict by network — simple and effective
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{
    ResponseWriter = WriteDetailedResponse
}).RequireHost("internal.myapp.com");

// Option 3: Minimal info for public endpoint
app.MapHealthChecks("/health", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("public"),
    ResponseWriter = (context, report) =>
    {
        context.Response.ContentType = "text/plain";
        return context.Response.WriteAsync(report.Status.ToString());
    }
});

A good pattern: expose a simple /health endpoint publicly (for load balancers) that returns only the status word, and a detailed /health/detail endpoint behind authentication for your operations team.

Configuring Timeouts and Intervals

A health check that takes 30 seconds to time out is worse than no health check — it blocks the endpoint and gives stale results. Always configure aggressive timeouts:

builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString: connectionString,
        name: "sql-server",
        timeout: TimeSpan.FromSeconds(3),  // Fail fast
        tags: new[] { "ready" })
    .AddCheck<PaymentGatewayHealthCheck>(
        "payment-gateway",
        timeout: TimeSpan.FromSeconds(5),
        tags: new[] { "ready" });

As a rule of thumb: health check timeouts should be significantly shorter than your probe intervals. If Kubernetes polls every 15 seconds, your health check should complete in under 5 seconds.

Publishing Health Check Results

Beyond the HTTP endpoints, you can push health status to external systems using IHealthCheckPublisher. This is useful for centralized monitoring when you can't rely on pull-based polling:

public class ApplicationInsightsPublisher : IHealthCheckPublisher
{
    private readonly TelemetryClient _telemetryClient;

    public ApplicationInsightsPublisher(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    public Task PublishAsync(
        HealthReport report, CancellationToken cancellationToken)
    {
        foreach (var entry in report.Entries)
        {
            _telemetryClient.TrackMetric(
                $"HealthCheck.{entry.Key}",
                entry.Value.Status == HealthStatus.Healthy ? 1 : 0);

            if (entry.Value.Status != HealthStatus.Healthy)
            {
                _telemetryClient.TrackEvent("HealthCheckFailed", new Dictionary<string, string>
                {
                    ["CheckName"] = entry.Key,
                    ["Status"] = entry.Value.Status.ToString(),
                    ["Description"] = entry.Value.Description ?? "N/A"
                });
            }
        }

        return Task.CompletedTask;
    }
}

// Register the publisher
builder.Services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(5);
    options.Period = TimeSpan.FromSeconds(30);
});

builder.Services.AddSingleton<IHealthCheckPublisher, ApplicationInsightsPublisher>();

Best Practices for ASP.NET Core Health Checks

After implementing health checks across dozens of production services, these are the patterns that matter most:

  • Separate liveness from readiness. A process that's alive but can't reach the database shouldn't be killed — it should be temporarily removed from rotation.
  • Set explicit timeouts. Default timeouts are too generous. A health check that hangs for 30 seconds cascades into load balancer timeouts and user-facing errors.
  • Don't check everything in one endpoint. Use tags to group checks by purpose. Your load balancer doesn't need to know if your email provider is down.
  • Use Degraded status wisely. Not every failure is catastrophic. If a caching layer is down, the app is slower but functional — that's Degraded, not Unhealthy.
  • Avoid expensive checks. A health check that runs a complex database query or triggers I/O-heavy operations can itself become a performance problem when polled frequently.
  • Secure detailed endpoints. Expose minimal information publicly. Detailed diagnostics belong behind authentication or on internal networks only.
  • Monitor the monitors. If your health check polling system goes down, you lose visibility. Ensure your observability stack itself has redundancy.
  • Test failure scenarios. Don't just test that checks return Healthy. Simulate database outages, network timeouts, and disk pressure to verify your checks detect real problems.

Common Pitfalls to Avoid

Even experienced teams make these mistakes with health check implementations:

  • Checking too many dependencies in the liveness probe. This causes unnecessary pod restarts when a downstream service blips. Liveness should only verify the process itself.
  • No caching of results. If you have 10 instances being polled every 10 seconds, that's 60 health check requests per minute hitting your database. Consider caching results for a short window.
  • Ignoring startup health checks. ASP.NET Core 8+ supports MapHealthChecks with a "startup" tag pattern. Use it to prevent traffic from reaching pods that haven't finished warming up.
  • Returning 200 for Degraded status. By default, both Healthy and Degraded return HTTP 200, while Unhealthy returns 503. Customize ResultStatusCodes if your load balancer needs to distinguish these.
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Degraded] = StatusCodes.Status200OK,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});

Conclusion

Implementing ASP.NET Core health checks is one of the highest-value, lowest-effort improvements you can make to a production application. In this guide, you've learned how to set up basic and custom health checks, return detailed JSON responses, configure Kubernetes liveness and readiness probes, secure your endpoints, and publish results to external monitoring systems.

The key takeaways: always separate liveness from readiness, set aggressive timeouts, use tags to organize checks by purpose, and secure detailed health information behind authentication. Start with database and critical API checks, then expand coverage as your monitoring maturity grows.

Health checks aren't just a devops checkbox — they're the foundation of observable, self-healing systems. Add them to your next deployment and you'll sleep better at 2 AM.

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

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...

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...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...