.NET Microservices — Health Checks & Readiness Probes
ASP.NET Health Endpoints for AKS/Container Apps
Introduction
Health checks are critical for container orchestration (Kubernetes, Azure Container Apps) to determine if your service is ready to receive traffic and if it's still healthy. Azure Kubernetes Service (AKS) uses health probes to make routing decisions:
- Liveness Probe — Is the container running? (restart if not)
- Readiness Probe — Can the container receive traffic? (add/remove from service)
- Startup Probe — Has the container started? (wait for initialization)
Why Health Checks Matter
Without Health Checks
User Requests → Service (Starting) → Fail/Timeout
→ Service (Degraded) → Slow/Fail
→ Service (DB Down) → Error
With Health Checks
Liveness: Every 10s, restart if failed
Readiness: Every 5s, remove from LB during problems
Startup: Wait 60s for init, then liveness takes over
User Requests → Service (Healthy) → OK
→ Service (Starting) → Wait
→ Service (Degraded) → Skip
ASP.NET Core Health Checks
Install Package
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.Client
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks
Basic Health Check Endpoint
// Program.cs
builder.Services.AddHealthChecks();
// Endpoints
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
// Only check if process is running
Predicate = _ => false
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
// Full check including dependencies
Predicate = check => check.Tags.Contains("ready")
});
Custom Health Checks
Database Health Check
public class SqlServerHealthCheck : IHealthCheck
{
private readonly string _connectionString;
public SqlServerHealthCheck(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
// Test query
await connection.ExecuteScalarAsync(
"SELECT 1",
cancellationToken: cancellationToken);
return HealthCheckResult.Healthy("Database connection successful");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Database connection failed",
ex);
}
}
}
Redis Health Check
public class RedisHealthCheck : IHealthCheck
{
private readonly IConnectionMultiplexer _redis;
public RedisHealthCheck(IConnectionMultiplexer redis)
{
_redis = redis;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var db = _redis.GetDatabase();
var pong = await db.PingAsync();
return HealthCheckResult.Healthy(
"Redis connection successful",
new Dictionary<string, object>
{
["latency"] = pong.TotalMilliseconds
});
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Redis connection failed", ex);
}
}
}
Service Bus Health Check
public class ServiceBusHealthCheck : IHealthCheck
{
private readonly ServiceBusClient _client;
public ServiceBusHealthCheck(ServiceBusClient client)
{
_client = client;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var admin = _client.GetNamespaceAdministrationClient();
await admin.GetQueueAsync("orders", cancellationToken);
return HealthCheckResult.Healthy("Service Bus accessible");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Service Bus not accessible", ex);
}
}
}
Register All Health Checks
// Program.cs
builder.Services.AddHealthChecks()
// Custom checks
.AddCheck<SqlServerHealthCheck>("sqlserver", tags: new[] { "db", "ready" })
.AddCheck<RedisHealthCheck>("redis", tags: new[] { "cache", "ready" })
.AddCheck<ServiceBusHealthCheck>("servicebus", tags: new[] { "messaging", "ready" })
// Built-in checks
.AddDbContextCheck<OrderDbContext>("efcore", tags: new[] { "db", "ready" })
.AddAzureBlobStorage("BlobStorageConnection", tags: new[] { "storage", "ready" })
.AddAzureServiceBusQueue("ServiceBusConnection", "orders", tags: new[] { "messaging", "ready" });
Health Check UI
Add UI Endpoint
// Program.cs
builder.Services.AddHealthChecksUI(options =>
{
options.SetEvaluationTimeInterval(TimeSpan.FromSeconds(30));
options.MaximumHistoryEntriesPerEndpoint(50);
options.AddApiEndpoint("/health-api", "API Health");
});
app.MapHealthChecksUI(options =>
{
options.UiPath = "health";
options.UiPathPrefix = "/health-ui";
});
appsettings.json
{
"HealthChecks-UI": {
"HealthChecks": [
{
"Name": "API Health",
"Uri": "https://localhost:5001/health-api"
},
{
"Name": "Order Service",
"Uri": "http://orders-service/health"
}
]
}
}
Kubernetes Probes Configuration
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
template:
spec:
containers:
- name: order-service
image: myregistry/order-service:1.0
ports:
- containerPort: 80
# Liveness: Restart if failing
livenessProbe:
httpGet:
path: /health/live
port: 80
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
# Readiness: Add to service when ready
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 3
# Startup: Wait for init to complete
startupProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30 # 30 * 5s = 150s max startup time
timeoutSeconds: 3
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Probe Settings Explained
| Setting | Liveness | Readiness | Startup |
|---|---|---|---|
| Purpose | Restart container | Add to service | Wait for init |
| initialDelaySeconds | 10s | 5s | 0 |
| periodSeconds | 10s | 5s | 5s |
| failureThreshold | 3 (30s) | 3 (15s) | 30 (150s) |
| timeoutSeconds | 5s | 3s | 3s |
Dependency Health with Details
Detailed Health Response
[ApiController]
[Route("health")]
public class HealthController : ControllerBase
{
private readonly HealthCheckService _healthCheckService;
public HealthController(HealthCheckService healthCheckService)
{
_healthCheckService = healthCheckService;
}
[HttpGet("ready")]
public async Task<IActionResult> GetReadiness()
{
var result = await _healthCheckService.CheckHealthAsync(
cancellationToken: HttpContext.RequestAborted,
tags: new[] { "ready" });
var response = new
{
status = result.Status.ToString(),
totalDuration = result.TotalDuration.TotalMilliseconds,
checks = result.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
duration = e.Value.Duration.TotalMilliseconds,
description = e.Value.Description,
exception = e.Value.Exception?.Message,
data = e.Value.Data
})
};
return result.Status switch
{
HealthStatus.Healthy => Ok(response),
HealthStatus.Degraded => StatusCode(503, response),
_ => StatusCode(503, response)
};
}
}
Custom Health Status
Aggregated Health with Business Metrics
public class BusinessHealthCheck : IHealthCheck
{
private readonly IOrderRepository _orderRepository;
private readonly IInventoryService _inventoryService;
public BusinessHealthCheck(
IOrderRepository orderRepository,
IInventoryService inventoryService)
{
_orderRepository = orderRepository;
_inventoryService = inventoryService;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var checks = new Dictionary<string, object>();
var isHealthy = true;
// Check pending orders
var pendingCount = await _orderRepository.GetPendingOrderCountAsync();
if (pendingCount > 1000)
{
checks["pendingOrders"] = pendingCount;
isHealthy = false;
}
// Check inventory
var lowStockCount = await _inventoryService.GetLowStockCountAsync();
if (lowStockCount > 50)
{
checks["lowStockItems"] = lowStockCount;
}
// Check database connection
try
{
await _orderRepository.GetAsync(Guid.Empty);
}
catch
{
isHealthy = false;
checks["database"] = "unhealthy";
}
var status = isHealthy ? HealthStatus.Healthy : HealthStatus.Degraded;
return new HealthCheckResult(
status,
"Business health check",
null,
checks);
}
}
Best Practices
| Practice | Description |
|---|---|
| Separate liveness/readiness | Different purposes |
| Fast checks only | No heavy operations in liveness |
| Include dependencies | Readiness should verify dependencies |
| Return details | Help with debugging |
| Use tags | Control which checks run when |
Azure Integration Hub - Advanced Level