Health Checks in .NET Aspire: Monitoring Service Readiness

Health checks answer a simple question: is this service working? In a distributed application, that question becomes more nuanced — your API might be running, but if the database is unreachable, it cannot serve requests. Aspire integrates deeply with .NET's health check system to give you visibility into the state of every service and its dependencies.

How Aspire Uses Health Checks

Aspire uses health checks in two key ways:

  1. The dashboard displays health status for every resource, updating in real time
  2. Resource dependencies use health checks to determine startup order — WaitFor will not proceed until the dependency reports healthy

The ServiceDefaults project sets up the baseline with AddDefaultHealthChecks and MapDefaultEndpoints, which we explored in an earlier article. Here we will focus on adding meaningful, specific health checks.

Adding Database Health Checks

The most common health check verifies database connectivity. Install the relevant NuGet package:

dotnet add package AspNetCore.HealthChecks.NpgSql

Register the check:

Example.cs
builder.Services.AddHealthChecks()
    .AddNpgSql(
        builder.Configuration.GetConnectionString("catalogdb")!,
        name: "postgresql",
        tags: ["ready"]);

The tags parameter is important. By convention, Aspire uses "live" for liveness checks (is the process running?) and "ready" for readiness checks (can the service handle requests?). A service can be alive but not ready — for example, while waiting for a database migration to complete.

Adding Multiple Checks

A service typically depends on several external systems. Register a check for each one:

Example.cs
builder.Services.AddHealthChecks()
    .AddNpgSql(
        builder.Configuration.GetConnectionString("catalogdb")!,
        name: "database",
        tags: ["ready"])
    .AddRedis(
        builder.Configuration.GetConnectionString("cache")!,
        name: "redis",
        tags: ["ready"])
    .AddRabbitMQ(
        builder.Configuration.GetConnectionString("messaging")!,
        name: "rabbitmq",
        tags: ["ready"])
    .AddCheck("self", () => HealthCheckResult.Healthy(),
        tags: ["live"]);

Custom Health Checks

When the built-in checks are not sufficient, write your own. Implement IHealthCheck:

Example.cs
public class PaymentGatewayHealthCheck : IHealthCheck
{
    private readonly HttpClient _httpClient;

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

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

            return response.IsSuccessStatusCode
                ? HealthCheckResult.Healthy("Payment gateway is reachable")
                : HealthCheckResult.Degraded("Payment gateway returned " +
                    $"{response.StatusCode}");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Payment gateway is unreachable", ex);
        }
    }
}

Register it:

Example.cs
builder.Services.AddHealthChecks()
    .AddCheck<PaymentGatewayHealthCheck>(
        "payment-gateway",
        tags: ["ready"]);

Mapping Health Check Endpoints

The ServiceDefaults project maps two endpoints by default. You can customise these or add additional endpoints:

Example.cs
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.MapHealthChecks("/alive", new HealthCheckOptions
{
    Predicate = r => r.Tags.Contains("live")
});

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

The UIResponseWriter (from AspNetCore.HealthChecks.UI.Client) returns a detailed JSON response showing the status of each individual check:

data.json
{
  "status": "Healthy",
  "totalDuration": "00:00:00.1234567",
  "entries": {
    "database": { "status": "Healthy", "duration": "00:00:00.0234567" },
    "redis": { "status": "Healthy", "duration": "00:00:00.0012345" },
    "rabbitmq": { "status": "Healthy", "duration": "00:00:00.0056789" }
  }
}

Health Checks and the Aspire Dashboard

The Aspire dashboard polls health check endpoints to display resource state. When a check transitions from healthy to unhealthy, the dashboard updates immediately. This gives you real-time visibility without needing to check each service manually.

Health Checks in Container Orchestration

When you deploy to Kubernetes or Azure Container Apps, health check endpoints serve as liveness and readiness probes:

config.yaml
livenessProbe:
  httpGet:
    path: /alive
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

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

The container orchestrator uses /alive to decide whether to restart the container and /ready to decide whether to route traffic to it. The same health checks that power the Aspire dashboard during development protect your application in production.

Health Check Timeouts

Set timeouts on individual checks to prevent a slow dependency from blocking the entire health check response:

Example.cs
builder.Services.AddHealthChecks()
    .AddNpgSql(
        connectionString,
        name: "database",
        timeout: TimeSpan.FromSeconds(3),
        tags: ["ready"]);

Health checks are the foundation of resilient distributed systems. Aspire makes them easy to set up, easy to visualise, and directly useful for both development and production deployment.