ASP.NET Core 10 APIs · Lesson 13

Liveness, readiness and real health endpoints

Separate a serving process from a dependency-readiness decision.

External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

A process that answers a probe may still be unready for work

Liveness and readiness are operational questions with different consequences. A basic liveness probe can establish that the process serves a health request, while readiness indicates whether the instance should receive the work its application expects. ASP.NET health checks provide real infrastructure for exposing those decisions, but the application's check must still observe what it claims to represent. This lesson deliberately uses an in-memory classroom switch rather than labeling a boolean as a real database probe. The portable companion displays the decision before and after that switch; the external project demonstrates real check registration and HTTP status mapping.

Follow the tagged readiness transition

The console prints live:healthy, ready:unhealthy and then ready:healthy. In the external server, the live endpoint excludes registered checks through its predicate and returns Healthy with status 200. The readiness endpoint selects registrations tagged ready. Its classroom-state check initially returns Unhealthy, which produces status 503. A Development-only POST changes the local switch and returns 204; the subsequent readiness request becomes Healthy with status 200. These observations show check selection and result mapping, not connectivity to any database, broker or remote dependency. The switch's limited purpose is explicit in its names and description.

Choose which checks answer each operational question

Tags and endpoint predicates let different probes select different registered checks. A critical dependency may belong in readiness without belonging in liveness, because restarting a healthy application repeatedly during a downstream outage can make recovery worse. Conversely, an empty-check liveness result does not prove every internal subsystem is functioning. Define the operational decision and its consumer before adding checks indiscriminately. This example has only one local state check, so it does not claim that its ready tag expresses every dependency a real deployment needs. A production check should inspect the actual contract named in its result.

The manual transition is not a deployment readiness API

The project refuses to start outside Development because its manual control is a teaching convenience, not an authenticated operational interface. Binding on loopback keeps the intended exercise local. The state uses explicit visibility operations for concurrent requests, but it still has no durable storage, dependency verification or orchestration integration. Do not copy the control into a deployment and describe a successful POST as proof that infrastructure is ready. Real checks need bounded work, appropriate cancellation and carefully chosen diagnostics; a detailed public probe can also disclose information unrelated callers do not need.

Practice: prove selection instead of assuming all checks run

Add a second deliberately unhealthy classroom check with a different tag. Predict the original live and ready responses before changing their predicates. Verify that the new registration affects only an endpoint that selects it. Then create a separate diagnostic endpoint that selects both checks and observe the combined result. Keep each predicate change explicit. The hint is that AddHealthChecks registration and MapHealthChecks selection are different boundaries, so one unhealthy registration does not necessarily imply every mapped health route returns the same status in this application.

Practice: replace the switch with a real bounded contract

Design a separate integration exercise for an actual owned dependency, naming the operation that determines readiness and the maximum time it may consume. Keep that adapter outside the portable console and use a controlled local fixture rather than a public service. Test available, unavailable and canceled-check outcomes independently. Decide which failure should remove readiness and which should affect liveness before writing code. Do not reuse the manual switch as the dependency implementation: its value is a deterministic policy example, while the integration test must observe the real resource and environment it claims to check.

Review the consequence of each health result

A health endpoint's meaning includes what an operator or orchestrator will do with its status. Preserve separate liveness and readiness expectations when adding checks, and avoid converting every dependency outage into an application restart condition. The complete project proves real check plumbing and an explicit state transition on a local Development server. The console proves only the decision table. Keep those limits visible when reporting validation. For production, choose check granularity, resource budgets, exposure and failure details according to the actual operational consumer instead of assuming that any route named health provides the needed semantics.

  • Name the actual resource or state a check observes; the classroom switch is not a database probe.
  • Select checks for liveness and readiness according to their different operational consequences.
External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Liveness, readiness and real health endpoints — real ASP.NET Core 10 project

Set up the SDK project

  1. Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
  2. From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
  3. Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
  4. Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
  5. GET /health/live and /health/ready; POST /lab/ready with an empty body, then GET readiness again. Keep this manual development control off any deployment.

Download complete SDK project ↗

Project files

WebLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

  </PropertyGroup>
</Project>
global.jsonDownload file ↗
{
  "sdk": {
    "version": "10.0.403",
    "rollForward": "latestPatch"
  }
}
Program.csDownload file ↗
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
if (!builder.Environment.IsDevelopment()) throw new InvalidOperationException("The manual readiness control is development-only");
builder.Services.AddSingleton<ReadyState>(); builder.Services.AddHealthChecks().AddCheck<ReadyCheck>("classroom-state", tags: new[] { "ready" });
var app = builder.Build();
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false });
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = registration => registration.Tags.Contains("ready") });
app.MapPost("/lab/ready", (ReadyState state) => { state.Ready = true; return TypedResults.NoContent(); }); app.Run();
public sealed class ReadyState { private int ready; public bool Ready { get => Volatile.Read(ref ready) == 1; set => Volatile.Write(ref ready, value ? 1 : 0); } }
public sealed class ReadyCheck(ReadyState state) : IHealthCheck {
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken token = default) {
        token.ThrowIfCancellationRequested(); return Task.FromResult(state.Ready ? HealthCheckResult.Healthy("Classroom switch is ready") : HealthCheckResult.Unhealthy("Classroom switch is not ready"));
    }
}

Expected result

On a fresh Development process, /health/live is 200 and /health/ready is 503. POST /lab/ready returns 204; readiness then becomes 200. This check observes an explicit in-memory classroom switch, not a real database.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

bool ready = false; Console.WriteLine("live:healthy"); Console.WriteLine("ready:" + (ready ? "healthy" : "unhealthy")); ready = true; Console.WriteLine("ready:" + (ready ? "healthy" : "unhealthy"));
Open C# practice

C# practice output

live:healthy
ready:unhealthy
ready:healthy
Try this
The readiness switch is explicitly in memory; production checks must inspect the dependency they claim to represent.

Further reading