Health checks give Kubernetes, load balancers, and monitoring systems a programmatic way to determine whether an API instance is healthy and ready to serve traffic. ASP.NET Core’s built-in health check system supports multiple check types (database connectivity, external services, custom business logic) and exposes them via HTTP endpoints. Kubernetes uses two separate probes: the liveness probe (is the process alive?) restarts a container on failure, and the readiness probe (is it ready to serve traffic?) temporarily removes it from load balancing. Getting these right prevents both stuck processes and premature traffic routing.
Health Check Configuration
// dotnet add package AspNetCore.HealthChecks.SqlServer
// dotnet add package AspNetCore.HealthChecks.Redis
// โโ Program.cs โ register health checks โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
builder.Services
.AddHealthChecks()
// Database connectivity check
.AddSqlServer(
builder.Configuration.GetConnectionString("Default")!,
name: "database",
tags: ["ready"]) // only included in readiness check
// Redis check
.AddRedis(
builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: ["ready"])
// Custom business logic check
.AddCheck<RequiredSeedDataHealthCheck>("seed-data", tags: ["ready"])
// Self (always healthy โ for liveness)
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]);
// โโ Map health check endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Liveness โ is the process alive? (no DB check โ it might be the DB that's broken)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live"),
ResponseWriter = WriteHealthResponse,
});
// Readiness โ is it ready to serve requests? (includes DB, Redis checks)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready") || check.Tags.Contains("live"),
ResponseWriter = WriteHealthResponse,
});
// Full health endpoint โ for internal monitoring only (protect from public)
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = WriteHealthResponse,
}).RequireAuthorization("HealthCheckPolicy");
// โโ Custom JSON response writer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
static Task WriteHealthResponse(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
duration = report.TotalDuration.TotalMilliseconds,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
duration = e.Value.Duration.TotalMilliseconds,
error = e.Value.Exception?.Message,
})
});
return context.Response.WriteAsync(result);
}
startupProbe: httpGet: path: /health/startup, failureThreshold: 30, periodSeconds: 10 โ gives up to 5 minutes for startup before Kubernetes gives up.Custom Health Check
// โโ Custom health check โ verifies required seed data exists โโโโโโโโโโโโโโ
public class RequiredSeedDataHealthCheck(AppDbContext db) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct = default)
{
try
{
var roleCount = await db.Roles.CountAsync(ct);
if (roleCount == 0)
return HealthCheckResult.Degraded(
"Required roles are missing โ seed data may not have been applied.");
var adminExists = await db.Users
.AnyAsync(u => db.UserRoles
.Any(ur => ur.UserId == u.Id &&
db.Roles.Any(r => r.Id == ur.RoleId && r.Name == "Admin")), ct);
return adminExists
? HealthCheckResult.Healthy("Seed data is present.")
: HealthCheckResult.Degraded("Admin user is missing from seed data.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Seed data check failed.", ex);
}
}
}
// Register: builder.Services.AddScoped<RequiredSeedDataHealthCheck>();
// Apply: .AddCheck<RequiredSeedDataHealthCheck>("seed-data", tags: ["ready"])
Common Mistakes
Mistake 1 โ Including database check in liveness probe (unnecessary container restarts)
โ Wrong โ liveness includes SQL check; DB is slow; container marked unhealthy and restarted unnecessarily.
โ Correct โ liveness checks process health only; readiness includes DB, Redis, external services.
Mistake 2 โ Exposing detailed health info publicly (infrastructure reconnaissance)
โ Wrong โ /health returns full check list including error messages; public internet can read service topology.
โ Correct โ require authentication for full health; public /health/live and /health/ready return only aggregate status.