Health Checks in ASP.NET Core and Integrating with Load Balancers

In modern cloud-native applications, ensuring your services are running correctly and can handle traffic is critical. ASP.NET Core health checks provide a standardized way to monitor application health and integrate seamlessly with load balancers, orchestrators, and monitoring systems. Whether you’re deploying to Azure App Service, Kubernetes, or behind an NGINX load balancer, implementing robust health checks is essential for maintaining high availability and reliability.
Table of Contents
- Understanding ASP.NET Core Health Checks
- Setting Up Basic ASP.NET Core Health Checks
- Implementing Multiple Health Check Endpoints
- Customizing Health Check Responses
- Integrating with Load Balancers
- Advanced Health Check Scenarios
- Monitoring and Observability
- Security Considerations
- Conclusion
Understanding ASP.NET Core Health Checks
ASP.NET Core health checks are built-in middleware components that expose endpoints to report the health status of your application and its dependencies. These endpoints return HTTP status codes that load balancers and orchestration platforms can interpret to make routing decisions. A healthy application returns 200 OK, while an unhealthy one returns 503 Service Unavailable.
The health check system is highly extensible, allowing you to monitor databases, external APIs, message queues, disk space, memory usage, and any custom business logic critical to your application’s operation. This is particularly important when implementing zero-downtime deployments where traffic needs to be routed away from instances during updates.
Setting Up Basic ASP.NET Core Health Checks
To get started with ASP.NET Core health checks, you’ll first need to add the health check services and middleware to your application. Here’s how to configure a basic health check endpoint:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add health check services
builder.Services.AddHealthChecks();
var app = builder.Build();
// Map health check endpoint
app.MapHealthChecks("/health");
app.Run();
This basic setup creates a health check endpoint at /health that returns 200 OK when the application is running. However, real-world applications need more sophisticated checks to verify that dependencies are also functioning correctly.
Adding Database Health Checks
Database connectivity is one of the most critical dependencies to monitor. Here’s how to add SQL Server health checks:
builder.Services.AddHealthChecks()
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("DefaultConnection"),
healthQuery: "SELECT 1;",
name: "sql-server",
failureStatus: HealthStatus.Degraded,
tags: new[] { "db", "sql" }
);
You’ll need to install the AspNetCore.HealthChecks.SqlServer NuGet package for this functionality. The healthQuery parameter allows you to specify a lightweight query to verify database connectivity without putting unnecessary load on your database.
Implementing Multiple Health Check Endpoints
For production environments, it’s best practice to expose multiple health check endpoints with different purposes. This approach is crucial when handling millions of users where you need fine-grained control over health monitoring:
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy("API is running"))
.AddSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"),
name: "database",
tags: new[] { "ready" }
)
.AddUrlGroup(
new Uri("https://external-api.example.com/health"),
name: "external-api",
tags: new[] { "ready" }
);
var app = builder.Build();
// Liveness endpoint - checks if app is running
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live") || check.Name == "self"
});
// Readiness endpoint - checks if app is ready to serve traffic
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
The liveness endpoint (/health/live) checks if your application process is running, while the readiness endpoint (/health/ready) verifies that all dependencies are available before accepting traffic. This separation is essential for container orchestration platforms like Kubernetes.
Customizing Health Check Responses
By default, health checks return simple HTTP status codes. However, you can customize the response to include detailed information about each health check:
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var response = new
{
status = report.Status.ToString(),
checks = report.Entries.Select(entry => new
{
name = entry.Key,
status = entry.Value.Status.ToString(),
description = entry.Value.Description,
duration = entry.Value.Duration.TotalMilliseconds
}),
totalDuration = report.TotalDuration.TotalMilliseconds
};
await context.Response.WriteAsJsonAsync(response);
}
});
This custom response writer provides detailed JSON output showing the status of each individual check, which is invaluable for debugging and monitoring dashboards.
Integrating with Load Balancers
Load balancers use health check endpoints to determine which application instances should receive traffic. Different load balancers have varying configuration requirements, but the principles remain consistent across platforms.
Azure Application Gateway Configuration
When deploying to Azure, Application Gateway can be configured to use your ASP.NET Core health checks endpoints. Here’s the recommended configuration for ASP.NET Core Web APIs:
- Health probe path:
/health/ready - Interval: 30 seconds
- Timeout: 30 seconds
- Unhealthy threshold: 3 consecutive failures
- Expected status code: 200
You can configure this through Azure Portal, Azure CLI, or Infrastructure as Code tools. The health probe continuously monitors your application instances and removes unhealthy ones from the load balancing pool.
Kubernetes Liveness and Readiness Probes
When containerizing your .NET applications with Docker and deploying to Kubernetes, configure both liveness and readiness probes in your deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
The liveness probe restarts the container if it becomes unresponsive, while the readiness probe prevents traffic from being routed to the pod until all dependencies are healthy. This configuration is part of implementing robust CI/CD pipelines for continuous deployment.
NGINX Load Balancer Integration
For NGINX-based load balancing, configure upstream health checks in your nginx.conf:
upstream backend {
server app1.example.com:8080;
server app2.example.com:8080;
server app3.example.com:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
health_check uri=/health/ready interval=10s fails=3 passes=2;
}
}
Note that active health checks in NGINX require NGINX Plus. For the open-source version, you’ll rely on passive health checks using the proxy_next_upstream directive.
Advanced Health Check Scenarios
Custom Health Checks
You can create custom health checks by implementing the IHealthCheck interface. Here’s an example that checks disk space availability:
public class DiskSpaceHealthCheck : IHealthCheck
{
private readonly string _driveName;
private readonly long _minimumFreeMegabytes;
public DiskSpaceHealthCheck(string driveName, long minimumFreeMegabytes)
{
_driveName = driveName;
_minimumFreeMegabytes = minimumFreeMegabytes;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var drive = DriveInfo.GetDrives()
.FirstOrDefault(d => d.Name.Equals(_driveName, StringComparison.OrdinalIgnoreCase));
if (drive == null)
{
return Task.FromResult(
HealthCheckResult.Unhealthy($"Drive {_driveName} not found"));
}
var freeMegabytes = drive.AvailableFreeSpace / 1024 / 1024;
if (freeMegabytes < _minimumFreeMegabytes)
{
return Task.FromResult(
HealthCheckResult.Unhealthy(
$"Insufficient disk space: {freeMegabytes}MB available"));
}
return Task.FromResult(
HealthCheckResult.Healthy($"Sufficient disk space: {freeMegabytes}MB available"));
}
catch (Exception ex)
{
return Task.FromResult(
HealthCheckResult.Unhealthy("Error checking disk space", ex));
}
}
}
// Register the custom health check
builder.Services.AddHealthChecks()
.AddCheck<DiskSpaceHealthCheck>(
"disk-space",
tags: new[] { "ready" });
builder.Services.AddSingleton(
new DiskSpaceHealthCheck("C:\\", minimumFreeMegabytes: 1024));
Caching Health Check Results
For expensive health checks that query external services, implement caching to avoid overwhelming dependencies:
builder.Services.AddHealthChecks()
.AddSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"),
name: "database",
tags: new[] { "ready" }
)
.AddCheck"external-api", () =>
{
// Implement caching logic here
return HealthCheckResult.Healthy();
},
tags: new[] { "ready" })
.AddCheck"memory", () =>
{
var allocated = GC.GetTotalMemory(forceFullCollection: false);
var threshold = 1024L * 1024L * 1024L; // 1 GB
var status = allocated < threshold
? HealthStatus.Healthy
: HealthStatus.Degraded;
return HealthCheckResult.Healthy(
$"Memory usage: {allocated / 1024 / 1024}MB");
});
Monitoring and Observability
Health checks are most effective when integrated with monitoring solutions. According to Microsoft’s official documentation, combining health checks with Application Insights or other monitoring platforms provides comprehensive observability into your application’s health status over time.
You can publish health check results to Application Insights or push them to monitoring systems like Prometheus for long-term trend analysis. This data helps identify patterns in application health degradation before they become critical issues.
Security Considerations
Health check endpoints can expose sensitive information about your application’s architecture and dependencies. Consider these security best practices:
- Use different endpoints for detailed vs. simple health checks
- Implement authentication for detailed health check endpoints
- Restrict detailed health information to internal networks only
- Avoid exposing connection strings or sensitive configuration in health check responses
- Use rate limiting to prevent health check endpoint abuse
// Public endpoint with minimal information
app.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = _ => false, // No checks, just returns 200 if app is running
AllowCachingResponses = false
});
// Internal endpoint with detailed information
app.MapHealthChecks("/health/detailed", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).RequireAuthorization("InternalOnly");
Conclusion
Implementing ASP.NET Core health checks with proper load balancer integration is fundamental to building resilient, production-ready applications. By exposing standardized health endpoints, you enable orchestrators and load balancers to make intelligent routing decisions, automatically removing unhealthy instances from rotation and ensuring high availability for your users.
Start with basic health checks for critical dependencies like databases and external APIs, then progressively add custom checks for business-critical components. Configure separate liveness and readiness endpoints to give orchestration platforms the granular control they need for effective traffic management. With these practices in place, your ASP.NET Core applications will be well-positioned to handle production workloads with confidence.
If you’re looking to build robust, enterprise-grade .NET applications with proper health monitoring and DevOps practices, WireFuture’s ASP.NET development services can help you implement these patterns correctly from the start. Our team specializes in building scalable cloud-native applications with comprehensive monitoring and observability built-in.
Imagine a team that sees beyond code—a team like WireFuture. We blend art and technology to develop software that is as beautiful as it is functional. Let's redefine what software can do for you.
No commitment required. Whether you’re a charity, business, start-up or you just have an idea – we’re happy to talk through your project.
Embrace a worry-free experience as we proactively update, secure, and optimize your software, enabling you to focus on what matters most – driving innovation and achieving your business goals.

