A health check that always returns 200 hides outages. A health check that calls SQL and fails the process restarts a healthy app because the database sneezed. Split the two questions.
Deploy notes: Azure App Service. Local containers: Docker with Angular.
Real-world analogy
A lighthouse tells ships the tower is standing. That is liveness. The harbour master tells ships whether a berth is free. That is readiness. If you put the harbour master on the lighthouse, a full harbour makes every tower look dead, and someone sends a crew to rebuild a tower that was fine.
Worked example
The live path calls AddDbContextCheck. SQL Server fails over for 40 seconds. Every instance returns 503 on liveness. The platform restarts all of them. They come back, hit the same failing check, and restart again. Move the database check to /health/ready and leave /health/live as "the process answered." During the failover, instances stay up and the load balancer stops sending traffic until SQL returns. No restart loop.
Two endpoints
| Endpoint | Question | Failure means | Put here |
|---|---|---|---|
/health/live | Is the process running? | Restart the container | A trivial check, or no checks |
/health/ready | Should the load balancer send traffic? | Remove this instance | SQL, Redis, required HTTP |
Do not expose stack traces or connection strings on either path. Health JSON is often unauthenticated.
Registration
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>(
name: "sql",
tags: ["ready"]);
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
AddDbContextCheck opens a connection. That is readiness. Tag it ready and keep it off /health/live.
If you have no live tags, the predicate above makes /health/live report healthy with zero checks. That is what you want: the process answered.
What not to check
- Do not HTTP-call another service on the liveness path. Use
IHttpClientFactoryon readiness, with a short timeout, and only if this API cannot work without that service. See IHttpClientFactory. - Do not check a cache that the app can skip. Redis down should not kill traffic if requests still succeed against SQL.
- Do not run a migration or a heavy query inside the check.
SELECT 1is enough.
Azure and Kubernetes
App Service health check expects HTTP 200 on the path you set. Point it at /health/ready. Unhealthy instances stop receiving requests. They do not restart unless you also wire liveness that way.
In Kubernetes, livenessProbe hits /health/live. readinessProbe hits /health/ready. Mixing them is the bug people google after a SQL failover restarts every pod at once.
Angular
The SPA does not call these endpoints. A browser poll of /health/ready adds load and leaks dependency names. Let the platform probe. The UI handles request failures through the same error envelope as every other call.