Back to blog
Backend Systemsintermediate

Health Checks — Know Your API Is Alive Before Kubernetes Does

Implement liveness, readiness, and startup probes in ASP.NET Core. Built-in DB checks, community NuGet packages, custom IHealthCheck, and Kubernetes probe config.

LearnixoApril 15, 20264 min read
.NETC#Health ChecksKubernetesASP.NET Core
Share:𝕏

The Three Probe Types

Kubernetes uses three probes to manage your pod lifecycle:

| Probe | Question | Failure action | |---|---|---| | Liveness | Is the process healthy (not deadlocked)? | Restart the pod | | Readiness | Is the service ready to accept traffic? | Remove from load balancer | | Startup | Has the app finished initialising? | Don't run liveness until this passes |

Map these to separate /health/* endpoints in your API.

Basic Setup

Bash
# No extra packages needed for core functionality
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.Client
dotnet add package AspNetCore.HealthChecks.SqlServer
dotnet add package AspNetCore.HealthChecks.Redis
dotnet add package AspNetCore.HealthChecks.Uris
C#
// Program.cs
builder.Services
    .AddHealthChecks()
    // EF Core database check
    .AddDbContextCheck<AppDbContext>("database", tags: ["ready"])
    // Direct SQL connection check
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default")!,
        name: "sql-server",
        tags: ["ready"])
    // Redis
    .AddRedis(
        builder.Configuration.GetConnectionString("Redis")!,
        name: "redis",
        tags: ["ready"])
    // External URL
    .AddUrlGroup(
        new Uri("https://api.stripe.com"),
        name: "stripe-api",
        tags: ["ready"]);

var app = builder.Build();

// Liveness — is the process alive? No dependency checks.
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false,   // run no checks, just return Healthy if process is up
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

// Readiness — are all dependencies reachable?
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

// Full — everything, for dashboards
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

Custom IHealthCheck

For checks that don't have a pre-built package — a message queue depth, a third-party service, a file system path:

C#
public class OrderQueueHealthCheck(IMessageQueue queue) : IHealthCheck
{
    private const int MaxQueueDepth = 10_000;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var depth = await queue.GetDepthAsync(cancellationToken);

            var data = new Dictionary<string, object>
            {
                ["queue_depth"] = depth,
                ["threshold"] = MaxQueueDepth
            };

            if (depth > MaxQueueDepth)
                return HealthCheckResult.Degraded(
                    $"Queue depth {depth} exceeds threshold {MaxQueueDepth}", data: data);

            return HealthCheckResult.Healthy($"Queue depth: {depth}", data);
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Cannot reach message queue", ex);
        }
    }
}

// Register it
builder.Services
    .AddHealthChecks()
    .AddCheck<OrderQueueHealthCheck>("order-queue", tags: ["ready"]);

HealthChecks UI Dashboard

C#
// Program.cs — services
builder.Services
    .AddHealthChecksUI(options =>
    {
        options.SetEvaluationTimeInSeconds(30);
        options.MaximumHistoryEntriesPerEndpoint(50);
        options.AddHealthCheckEndpoint("API", "/health");
    })
    .AddInMemoryStorage();   // or .AddSqlServerStorage(connStr) for persistence

// Program.cs — middleware
app.MapHealthChecksUI(config => config.UIPath = "/health-ui");

Browse to /health-ui for a live dashboard showing status history for every check.

Kubernetes Probe Configuration

YAML
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: api
          image: myregistry/api:latest
          ports:
            - containerPort: 8080

          startupProbe:
            httpGet:
              path: /health/live
              port: 8080
            failureThreshold: 30     # 30 * 10s = 5 min to start
            periodSeconds: 10

          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 0
            periodSeconds: 10
            failureThreshold: 3      # restart after 3 failures

          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3      # pull from LB after 3 failures

Key points:

  • startupProbe prevents liveness killing a slow-starting app (EF Core migrations, cache warm-up)
  • Liveness uses /health/live (no deps) — you don't want a Redis blip to restart your pod
  • Readiness uses /health/ready (deps checked) — a DB outage should remove the pod from rotation

Securing the Health Endpoint

Health responses can leak connection strings and internal URLs. Restrict access:

C#
app.MapHealthChecks("/health")
   .RequireAuthorization("InternalOnly");

// or restrict to cluster-internal IPs only via network policy,
// and return minimal output to the public:
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false,
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
    // No ResponseWriter — returns plain 200 or 503, no JSON body
});

Summary

  • Three endpoints: /health/live (process alive), /health/ready (deps reachable), /health (full detail)
  • Tag checks with "ready" and filter by tag so liveness never touches external dependencies
  • Custom IHealthCheck covers anything not in the NuGet ecosystem — queue depth, disk space, license validity
  • Match Kubernetes probe paths exactly to your mapped endpoints; use startupProbe for apps with slow cold-start

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.