Minimal ASP.NET Core Health Checks for Microservices: Architecture, Design, and Operational Guidance
Implement a minimal, secure health‑check endpoint in ASP.NET Core microservices. Learn the smallest architecture, protect the endpoint, differentiate liveness and readiness, and know when to redesign.
26 Nov 2025, 06:57 UTC

Problem Statement
Microservice deployments often expose a /health endpoint to let orchestrators and monitoring systems verify that the service is running and can access its critical dependencies. The challenge is to implement this endpoint in the smallest, safest way possible, respecting security boundaries and operational constraints.
Functional Requirements
- Return a JSON payload with an overall status (Healthy, Degraded, Unhealthy) and optional per‑check details.
- Differentiate between liveness (service can run) and readiness (service can serve traffic).
- Protect the endpoint from accidental exposure to unauthenticated users.
- Avoid expensive or blocking operations that could degrade throughput.
- Provide hooks for metrics collection (e.g., Prometheus, Application Insights).
Smallest Viable Architecture
The core components are:
- HealthCheck Registration – using
IHealthChecksBuilderinProgram.cs. - Endpoint Configuration –
app.UseHealthChecks("/health", options)withHealthCheckOptions. - Optional Authorization Middleware – to restrict access.
- Optional Metrics Exporter – e.g.,
app.UseHealthChecksPrometheusScrapingEndpoint().
Trust & Data Boundaries
Health checks must never leak sensitive configuration or stack traces. Use the ResultDescription field sparingly and avoid including connection strings or secrets. The endpoint should either be behind an API gateway that enforces authentication or protected by a simple bearer token validated by the middleware.
Operational Monitoring
Deploy the health endpoint as the target of Kubernetes liveness and readiness probes:
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/readiness
port: 80
initialDelaySeconds: 5
periodSeconds: 5
For metrics, expose a Prometheus scrape endpoint:
app.UseHealthChecksPrometheusScrapingEndpoint();
Failure Modes
| Mode | Trigger | Impact |
|---|---|---|
| Database Unavailable | DB connection fails | Readiness = Unhealthy, Liveness = Healthy |
| Cache Outage | Redis ping fails | Readiness = Degraded, Liveness = Healthy |
| Code Bug | Unhandled exception during health check | Both probes = Unhealthy |
When to Redesign
- Health checks start to consume significant CPU or memory.
- More than three or four dependencies are required, making the endpoint too complex.
- Security reviews reveal that the endpoint is exposed to public networks.
- Metrics show persistent high latency (>300 ms) on the health endpoint.
Example Implementation
Program.cs (minimal API)
var builder = WebApplication.CreateBuilder(args);
// Register health checks
builder.Services.AddHealthChecks()
.AddSqlServer(builder.Configuration.GetConnectionString("Default"), name: "sql", failureStatus: HealthStatus.Unhealthy, tags: new[] { "ready" })
.AddRedis(builder.Configuration["Redis:ConnectionString"], name: "redis", failureStatus: HealthStatus.Degraded, tags: new[] { "ready" });
var app = builder.Build();
// Protect /health with a simple bearer token
app.Use(async (context, next) => {
if (context.Request.Path.StartsWithSegments("/health")) {
if (!context.Request.Headers.TryGetValue("Authorization", out var token) || token != "Bearer secret-token") {
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
return;
}
}
await next();
});
// Liveness probe – only essential checks
app.UseHealthChecks("/health", new HealthCheckOptions {
Predicate = check => check.Tags.Contains("live"),
ResponseWriter = async (ctx, report) => {
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsync($"{{\"status\":\"{report.Status}\"}}");
}
});
// Readiness probe – all critical dependencies
app.UseHealthChecks("/health/readiness", new HealthCheckOptions {
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = async (ctx, report) => {
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsync($"{{\"status\":\"{report.Status}\",\"results\":{JsonSerializer.Serialize(report.Entries)}}}");
}
});
app.Run();
Health Check Tags Explained
live– minimal checks that must pass for the service to be considered running.ready– critical dependencies that must be available before routing traffic to the service.
Testing & Verification
- Unit test the health endpoint by sending HTTP GET requests and asserting status codes 200 (Healthy), 503 (Unhealthy), etc.
- In a staging Kubernetes cluster, configure liveness/readiness probes as shown and verify pod restarts only on true liveness failures.
- Enable Prometheus scraping and observe
health_check_duration_secondsandhealth_check_statusmetrics. - Run a load test on the health endpoint to confirm latency stays below 50 ms under typical traffic.
Summary
By registering only the essential checks, protecting the endpoint, and separating liveness from readiness, a microservice can expose a lightweight, secure health API that satisfies orchestrators and monitoring systems without impacting performance. Keep the design simple; if the endpoint grows beyond a handful of checks or starts to degrade service metrics, consider refactoring into dedicated health services or moving non‑critical checks to background jobs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.