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

Tapesh Mehta Tapesh Mehta | Published on: Feb 14, 2026 | Est. reading time: 8 minutes
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

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.

Share

clutch profile designrush wirefuture profile goodfirms wirefuture profile
Software Development, Reimagined! 🎨

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.

Hire Now

Categories
.NET Development Angular Development JavaScript Development KnockoutJS Development NodeJS Development PHP Development Python Development React Development Software Development SQL Server Development VueJS Development All
About Author
wirefuture - founder

Tapesh Mehta

verified Verified
Expert in Software Development

Tapesh Mehta is a seasoned tech worker who has been making apps for the web, mobile devices, and desktop for over 15+ years. Tapesh knows a lot of different computer languages and frameworks. For robust web solutions, he is an expert in Asp.Net, PHP, and Python. He is also very good at making hybrid mobile apps, which use Ionic, Xamarin, and Flutter to make cross-platform user experiences that work well together. In addition, Tapesh has a lot of experience making complex desktop apps with WPF, which shows how flexible and creative he is when it comes to making software. His work is marked by a constant desire to learn and change.

Get in Touch
Your Ideas, Our Strategy – Let's Connect.

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.

Hire Your A-Team Here to Unlock Potential & Drive Results
You can send an email to contact@wirefuture.com
clutch wirefuture profile designrush wirefuture profile goodfirms wirefuture profile good firms award-4 award-5 award-6