Skip to content

Health probes: /health and /ready

Core ships both. Do not write your own — every app repo that did so ended up maintaining a near-identical copy, and the reason they did was that the only probe used to be /ready, which was not safe to point a monitor at.

Both are mounted by core with no authentication: a monitor cannot log in.

/health /ready
Question Is the process serving? Can it do useful work?
Checks nothing initialised, plus the database answers
Degraded app 200 503
Still starting 200 503
Body {"status": "ok"}, plus "build" when SOURCE_COMMIT_APP is set {"status": "ready"} / {"status": "not_ready", "reason": …}
Point it at container liveness, uptime monitors load balancers, readiness gates, deploy checks

Why two

They answer different questions, and one endpoint cannot answer both.

An orchestrator restarts a container whose liveness probe fails. So liveness must fail only when a restart is the right response. A degraded FastPluggy app — one that booted without its database — is live: the process is serving, and the degraded-mode reprobe may heal it seconds later without help. Report that as "dead" and the orchestrator kills it on every check, turning a recoverable outage into a crash loop.

A load balancer, meanwhile, needs the opposite: stop sending traffic the moment the app cannot serve a request, and resume when it can. That is /ready.

This is why /health takes no dependency at all — not the database, not the plugin registry, not even the FastPluggy instance. Anything it touches is something that can fail and trigger a pointless restart.

What they must never return

Both are unauthenticated, so neither returns plugin names, counts, versions, settings, or any connection detail — with one deliberate exception, the build identifier below. /ready's reason is a fixed word from a closed set (starting, database) and never the driver's error message — connection errors routinely carry the host, the database name, and sometimes credentials.

If you need richer diagnostics, put them behind authentication. An open endpoint is the wrong place to answer "which database am I connected to".

Availability during a failed boot

/health is registered in FastPluggy.__init__, before the database is first probed, and the degraded-mode middleware exempts it. That is deliberate: a boot that fails never reaches load_app(), so a probe registered there would return 404 in exactly the situation the orchestrator is asking about — and a 404 is as much a failure as a 503.

/ready no longer triggers a reload

It used to call trigger_reload(plugins_dir), which unlinks a marker file in the plugins directory on every request. Under --reload that unlink is a filesystem event, so polling readiness restarted the app; without --reload it was a stray unlink on every poll. A probe reports state, it does not change it. trigger_reload is unchanged and still available to the callers whose job is actually reloading.

If you were relying on GET /ready as a way to force a reload in development, call trigger_reload directly or restart the dev server.

The one admitted field: build (core >= 0.4.65)

/health echoes a build identifier when the env var SOURCE_COMMIT_APP is set, and is silent when it is not. Set nothing and the body is byte-identical to what it always was.

{"status": "ok", "build": "a1b2c3d"}

It is admitted where versions are refused because it is an identity, not a version: a commit SHA of a private repo does not tell an unauthenticated caller which release you run or which advisories apply to it, the way "FastPluggy 0.4.60" would. It answers the one question a bare {"status": "ok"} cannot — is what I just deployed actually running? — which otherwise costs a registry digest comparison and still leaves doubt.

The name is not a preference. Nine app repos already set SOURCE_COMMIT_APP, so core adopting a different one would have left every one of them rolling its own probe. build_identifier() reads the environment per call, never cached at import: a value captured before the environment was assembled would outlive the truth.

The same identity as a metric: fp_core_build_info

core_metrics.py emits the standard *_build_info shape — a constant 1 whose labels carry the information — so the running build can be joined onto any other series ("alert when the running build != the expected one") in a way a JSON body never can. Emitted only when the variable is set, same rule as the probe.

The metrics router auto-prefixes every core metric name with fp_core_. The entry is registered as build_info and scrapes as fp_core_build_info — not fastpluggy_build_info, which is the name issue #29 proposed before the prefixing convention was checked. Never start a name in core_metrics.py with fp_, or it scrapes as fp_core_fp_….