Two health endpoints ship out of the box: GET /health/live for liveness, GET /health/ready for readiness. Both are anonymous and exempt from rate limiting. The kit registers checks for every dependency the host needs to function — one database check per module DbContext, Valkey (a Redis-compatible, BSD-licensed Redis fork) when caching is enabled, Hangfire when jobs are enabled, and a per-tenant migration check that reports whether every tenant DB is on the latest schema.
What’s checked
| Check | Source | When registered |
|---|---|---|
self | AddHeroPlatform | Always |
db:{module} (e.g. db:identity, db:catalog, db:billing…) | AddDbContextCheck<T> in each module’s registration | One per module DbContext |
redis | Kit’s RedisHealthCheck | When EnableCaching = true and CachingOptions:Redis is set |
hangfire | Kit’s HangfireHealthCheck | When EnableJobs = true |
db:tenants-migrations | Kit’s TenantMigrationsHealthCheck (Multitenancy module) | Always (with the Multitenancy module) |
There is no storage/MinIO health check — blob storage failures surface through the Files module’s own error handling, not readiness.
How to wire probes
Kubernetes
livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 30 periodSeconds: 10readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 5Docker Compose
services: api: image: fsh-api:latest healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"] interval: 10s timeout: 5s retries: 3Cloud load balancers
Point them at /health/ready — they’ll only route traffic to instances that report ready.
Response shape
The kit serializes its own compact shape (not the default ASP.NET Core health JSON):
GET /health/live HTTP/1.1
HTTP/1.1 200 OKContent-Type: application/json
{ "status": "Healthy", "results": [] }GET /health/ready HTTP/1.1
HTTP/1.1 200 OKContent-Type: application/json
{ "status": "Healthy", "results": [ { "name": "self", "status": "Healthy", "description": null, "durationMs": 0.01 }, { "name": "db:identity", "status": "Healthy", "description": null, "durationMs": 12.3 }, { "name": "db:catalog", "status": "Healthy", "description": null, "durationMs": 8.7 }, { "name": "redis", "status": "Healthy", "description": null, "durationMs": 1.9 }, { "name": "db:tenants-migrations", "status": "Healthy", "description": "All tenants are at the head migration.", "durationMs": 41.0, "details": { /* per-tenant data */ } } ]}When any check fails, the overall response is HTTP 503 — with the same body shape, so operators can see exactly which check failed while probe consumers key off the status code alone.
Adding your own check
Implement IHealthCheck and register it during module startup:
public sealed class CustomerApiHealthCheck(HttpClient http) : IHealthCheck{ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct) { try { using var resp = await http.GetAsync("/health", ct).ConfigureAwait(false); return resp.IsSuccessStatusCode ? HealthCheckResult.Healthy() : HealthCheckResult.Degraded($"Upstream returned {(int)resp.StatusCode}"); } catch (Exception ex) { return HealthCheckResult.Unhealthy("Upstream unreachable", ex); } }}
// register in the module's ConfigureServicesservices.AddHealthChecks().AddCheck<CustomerApiHealthCheck>("customer-api");Every check registered with AddHealthChecks() automatically participates in /health/ready — liveness runs no checks, so there’s nothing to tag or filter. Keep slow checks (cross-region pings, third-party APIs) out of the registry or make them cheap; readiness latency is probe latency.
Tenant migrations check
TenantMigrationsHealthCheck is unusual — it iterates every tenant, sets that tenant’s Finbuckle context, and asks EF Core for pending migrations. Per-tenant detail lands in the check’s data:
{ "name": "db:tenants-migrations", "status": "Unhealthy", "description": "Tenant schema is not at head — pending migrations for tenant(s): acme. Run FSH.Starter.DbMigrator to apply pending migrations.", "details": { "acme": { "name": "Acme", "isActive": true, "hasPendingMigrations": true, "pendingMigrations": ["20260601_AddX"] } }}A tenant with pending migrations (or one whose probe throws) makes the check Unhealthy, which makes /health/ready return 503 — Kubernetes keeps the pod out of rotation until the standalone FSH.Starter.DbMigrator catches the schema up. That’s deliberate: the DB is never migrated at API startup, so a pod running newer code against an older schema must not take traffic.
What health checks don’t tell you
- Per-endpoint health —
/health/readyreports infrastructure state, not “is this specific endpoint working.” Use synthetic monitoring (probing real endpoints with known payloads) for that. - Latency — health is binary; latency is a spectrum. Use the OpenTelemetry HTTP server metrics for latency SLOs.
- Partial tenant outages — the migrations check covers schema drift, but a single tenant’s connectivity problem in a tenant-per-database setup needs its own aggregation if your SLA covers per-tenant availability.
Related
- Observability — for latency + error-rate signals.
- Deployment guide — how the probes are wired in the kit’s docker-compose and Kubernetes manifests.
- Multitenancy module — owns the per-tenant migration check.