Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions src/dotnet/skills/implementing-health-checks/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
name: implementing-health-checks
description: Implement ASP.NET Core health checks with liveness, readiness, and startup probes for Kubernetes and load balancer integration. Use when configuring health endpoints, monitoring dependencies, or setting up container orchestration probes.
---

# Implementing Health Checks

## When to Use

- Adding health check endpoints to an ASP.NET Core app
- Configuring Kubernetes liveness, readiness, and startup probes
- Monitoring database, cache, or external service availability
- Load balancer health endpoint configuration

## When Not to Use

- The app is not ASP.NET Core
- The user wants application performance monitoring (use OpenTelemetry instead)
- The user needs business-level monitoring (use custom metrics)

## Inputs

| Input | Required | Description |
|-------|----------|-------------|
| ASP.NET Core project | Yes | The project to add health checks to |
| Dependencies to monitor | No | Database, Redis, message queue, etc. |

## Workflow

### Step 1: Add the health checks packages

```bash
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package Microsoft.Extensions.Diagnostics.HealthChecks is already included in ASP.NET Core framework references and doesn't need to be explicitly installed. While this line isn't harmful, it's redundant and could be removed to avoid confusion.

Suggested change
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks

Copilot uses AI. Check for mistakes.
dotnet add package AspNetCore.HealthChecks.SqlServer # for SQL Server
dotnet add package AspNetCore.HealthChecks.Redis # for Redis
dotnet add package AspNetCore.HealthChecks.Uris # for HTTP dependencies
```

### Step 2: Register health checks with SEPARATE liveness and readiness

**Critical distinction** most implementations get wrong:

- **Liveness** = "Is the process alive?" — Only checks the process isn't deadlocked. Failure → Kubernetes RESTARTS the pod.
- **Readiness** = "Can the process serve traffic?" — Checks dependencies. Failure → Kubernetes STOPS SENDING traffic (but doesn't restart).
- **Startup** = "Has the initial startup completed?" — One-time check. Failure during grace period is expected.

```csharp
builder.Services.AddHealthChecks()
// Liveness checks: ONLY check the process itself, NEVER external dependencies
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })

// Readiness checks: check external dependencies
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("Default")!,
name: "database",
tags: new[] { "ready" })
.AddRedis(
redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: new[] { "ready" })
.AddUrlGroup(
new Uri("https://api.external-service.com/health"),
name: "external-api",
tags: new[] { "ready" });
Comment on lines +56 to +64

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Common Pitfalls section recommends adding timeout parameters to health check registrations, but the example code in Step 2 doesn't include any timeout parameters. For consistency and to demonstrate best practices, consider adding timeout parameters to the health check registrations in Step 2. For example: .AddSqlServer(connectionString: ..., name: "database", tags: new[] { "ready" }, timeout: TimeSpan.FromSeconds(5))

Suggested change
tags: new[] { "ready" })
.AddRedis(
redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: new[] { "ready" })
.AddUrlGroup(
new Uri("https://api.external-service.com/health"),
name: "external-api",
tags: new[] { "ready" });
tags: new[] { "ready" },
timeout: TimeSpan.FromSeconds(5))
.AddRedis(
redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: new[] { "ready" },
timeout: TimeSpan.FromSeconds(5))
.AddUrlGroup(
new Uri("https://api.external-service.com/health"),
name: "external-api",
tags: new[] { "ready" },
timeout: TimeSpan.FromSeconds(5));

Copilot uses AI. Check for mistakes.
```

### Step 3: Map separate health endpoints

```csharp
// Liveness: Kubernetes livenessProbe hits this
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live"),
ResponseWriter = WriteMinimalResponse
});

// Readiness: Kubernetes readinessProbe hits this
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = WriteDetailedResponse
});

// Startup: Kubernetes startupProbe hits this
app.MapHealthChecks("/healthz/startup", new HealthCheckOptions
{
Predicate = _ => true,

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startup probe configuration uses Predicate = _ => true, which executes ALL registered health checks including database, Redis, and external API dependency checks. This contradicts Kubernetes best practices: startup probes should check the same thing as liveness probes (process health only), not readiness checks (dependencies). The current implementation means if any dependency is unavailable during the 150-second startup window, Kubernetes will kill the pod rather than marking it as "not ready". Consider changing to Predicate = check => check.Tags.Contains("live") to align with the liveness/readiness separation pattern explained in Step 2.

Suggested change
Predicate = _ => true,
Predicate = check => check.Tags.Contains("live"),

Copilot uses AI. Check for mistakes.
ResponseWriter = WriteMinimalResponse
});
```

### Step 4: Write response formatters

```csharp
static Task WriteMinimalResponse(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var result = new { status = report.Status.ToString() };
return context.Response.WriteAsJsonAsync(result);
}

static Task WriteDetailedResponse(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var result = new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds
})
};
return context.Response.WriteAsJsonAsync(result);
}
```

### Step 5: Configure Kubernetes probes

```yaml
# In the Kubernetes deployment spec:
containers:
- name: myapp
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 0 # Start checking immediately
periodSeconds: 10
failureThreshold: 3 # Restart after 3 failures
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3 # Stop traffic after 3 failures
startupProbe:
httpGet:
path: /healthz/startup
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30 # Allow up to 150s for startup
```

### Step 6: Add health check UI (optional)

```bash
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
```

```csharp
builder.Services.AddHealthChecksUI().AddInMemoryStorage();
app.MapHealthChecksUI();
```

## Common Pitfalls

| Pitfall | Solution |
|---------|----------|
| Checking DB in liveness probe | DB down → pod restarts → makes outage worse. DB checks go in READINESS only |
| No timeout on health checks | Add `timeout: TimeSpan.FromSeconds(5)` to each check registration |
| Health endpoint not excluded from auth | Add `.AllowAnonymous()` to `MapHealthChecks` or exclude path in auth middleware |
| Startup probe missing | Without it, liveness probe kills pods during slow cold starts |
| All checks on one endpoint | Separate live/ready/startup — mixing them causes cascading restarts |
31 changes: 31 additions & 0 deletions src/dotnet/tests/implementing-health-checks/eval.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
scenarios:
- name: "Add health checks with Kubernetes probes"
prompt: "I need to add health checks to my ASP.NET Core API for Kubernetes deployment. It should check the database and Redis connections and have separate liveness and readiness endpoints."
assertions:
- type: "output_matches"
pattern: "(AddHealthChecks|MapHealthChecks)"
- type: "output_matches"
pattern: "(liveness|readiness|live|ready)"
- type: "output_matches"
pattern: "(healthz|health)"
rubric:
- "Separated liveness and readiness health checks using tags"
- "Liveness probe does NOT check external dependencies (database, Redis) — only process health"
- "Readiness probe checks database and Redis connectivity"
- "Mapped separate endpoints for liveness and readiness (e.g., /healthz/live and /healthz/ready)"
- "Explained WHY liveness should not check dependencies (restart cascading)"
- "Provided Kubernetes probe YAML configuration or explained probe settings"
expect_tools: ["bash"]
timeout: 120

- name: "Health check skill should not activate for monitoring setup"
prompt: "I want to add application performance monitoring with OpenTelemetry to track request latency and error rates."
assertions:
- type: "output_not_contains"
value: "AddHealthChecks"
- type: "output_not_contains"
value: "MapHealthChecks"
rubric:
- "Did NOT suggest health checks for an APM/observability request"
- "Focused on OpenTelemetry setup (tracing, metrics, exporters)"
timeout: 60
Loading