Skip to content

Discovery

How module-gating decides which indicators run in a given binary — and what to check when one goes silently inert.

#[indicators] submits each tagged method to a link-time registry. At probe time, HealthService drains that registry and filters it against ReachableProviders — the access-graph-derived set of every provider reachable from the running app’s root module. The same module-gating every transport uses for #[controller], #[resolver], or #[processor] applies here.

Provider stateIndicator behavior
Provider lives in a module imported by AppModule (directly or transitively)Runs on every matching probe
Provider linked into the binary but in no reachable moduleSkipped; named once at boot with a warn on nest_rs::health, so it shows at any production filter

Linking a crate without importing its module keeps every provider in that crate inert — present in the binary, not mounted on any transport. Health indicators inherit that contract: a worker app that imports UsersModule but not UsersHealthModule does not run the users indicator, even though the symbol is in the binary.

Two reasons.

Per-app subsets. crates/features/ ships every adapter a feature can have — HTTP controller, GraphQL resolver, queue processor, health indicator. An app picks which ones it serves by importing the matching modules. The worker binary stays free of HTTP surface even though it links the feature crate. Health is no different: the indicator only runs where the app asked for it.

No surprise checks. An indicator is a check, and a check on the wrong app is worse than no check. A startup probe that pings a database an MCP-only worker doesn’t even own would drop the worker’s /health/startup to 503 forever. Module-gating keeps the check where its provider belongs.

When an indicator silently sits out, the framework names it once at boot on the nest_rs::health target — the same contract every other discovery seam honours (nest_rs::queue, nest_rs::events):

WARN nest_rs::health: skipped indicator: no instance of the provider in this
app's container indicator="upstream" kind=Readiness
origin="features::upstream::health" hint="nothing is registered under that
exact type. Common causes: …"

warn for your indicator, so it shows at any production filter, and once — a wiring notice repeated on every probe is log volume, not a signal. An indicator a nest-rs-* crate ships and this app never opted into reports at debug instead: it is not your mistake and you cannot act on it, so a DB-less app is not told twice per boot to go bind nest_rs_seaorm’s. Probing then skips in silence. The usual cause is a missing <Feature>HealthModule import in AppModule — the feature’s data module imports the service, not the health adapter.

A second check: the response body. With the indicator’s module imported, the indicator’s name appears under details on its probe:

Terminal window
$ curl -s http://localhost:3000/health/ready | jq '.details | keys'
[ "db", "upstream" ]

Missing key, missing import. Present key with status: "down", genuine failure — read the error bucket.

  • Indicators — where they live and how to write one.
  • Providers — the access graph, the reachable set, and what makes a provider reachable.
  • Modules — imports = [...], idempotent registration, the port + adapters layout.