Checking Health over HTTP
An orchestrator such as Kubernetes regularly asks an application whether it can serve requests, and whether it is alive. To answer both by the state of the projections, hand the runs started with start_projection, start_resumable_projection, or start_transactional_projection over to Health::readiness and Health::liveness. A Health is a handler of axum, so route it with get on paths of your choice.
They list each projection by the name it was given with ProjectionOptions::named:
use architecturekit::{ProjectionOptions, Subject, Subjects, httpapi::Health};
use axum::routing::get;
let run = store.start_projection(
Subjects::tree(Subject::new("/books")),
catalog_projection,
token.clone(),
ProjectionOptions::named("catalog"),
);
let router: Router = Router::new()
.route("/ready", get(Health::readiness([run.clone()])))
.route("/live", get(Health::liveness([run.clone()])));
A run can be cloned, and every clone watches the same run, so both handlers watch it, and every answer tells where it stands at that moment.
Note that a run without a name, or two runs of the same name, make Health::readiness and Health::liveness panic, since the answer tells the projections apart by their names.
Both answer with 200 OK or 503 Service Unavailable, depending on where the projections stand:
| Projection | readiness |
liveness |
|---|---|---|
| catches up for the first time | 503 |
200 |
| can not reach the database at the start | 503 |
200 |
| is live | 200 |
200 |
| reconnects after it has caught up | 200 |
200 |
| has stopped | 503 |
503 |
The application is ready once every projection has caught up, since a half-built view answers wrongly. A projection that reconnects later on, for example because the database restarts, keeps it ready: its view is behind, but consistent, and every instance shares the database, so taking them all out would answer nothing instead of something that is behind. A projection that has stopped makes the application neither ready nor alive, since its view never changes again. The orchestrator then restarts the application, which builds the view anew, with a configuration that may have been fixed in the meantime. There is no time limit for reconnecting, since a restart does not bring the database back.
The body of readiness tells whether the application is ready, and where each projection stands:
{
"isReady": true,
"projections": {
"catalog": {
"attempts": 0,
"hasCaughtUp": true,
"phase": "live",
"revision": "42",
"since": "2026-09-30T12:00:00.119521Z"
}
}
}
The body of liveness has the same shape, but tells whether the application is alive, with isAlive instead of isReady. Both answers say Cache-Control: no-store, so that no cache keeps them.
The body does not tell why a projection reconnects or has stopped, since health checks are usually reachable without signing in, and the reason may name internal addresses. Log it instead, for example by waiting for done and asking for error, here with the log crate, which you add with cargo add log:
run.done().await;
if let Some(error) = run.error() {
log::error!("the catalog has stopped: {error}");
}