Skip to content

Health Checks

wiki edited this page Sep 4, 2026 · 1 revision

Health checks

type HealthCheck interface {
	Name() string
	Execute(ctx context.Context) *CheckResult
	Timeout() time.Duration
	IsReadiness() bool
	Tags() []string
	Mode() CheckMode
	CacheTTL() time.Duration
}

You rarely implement it. health.NewCheck builds one from a function.

Writing one

check := health.NewCheck("database",
	func(ctx context.Context, r rx.Resolver) *health.CheckResult {
		var db *sql.DB
		if err := r.Resolve(&db); err != nil {
			return health.NewCheckResult(health.StatusDown, "not wired", 0)
		}
		start := time.Now()
		if err := db.PingContext(ctx); err != nil {
			// The message is internal-facing — it appears on /status, which
			// should not be public. Do not put it in a client response.
			return health.NewCheckResult(health.StatusDown, err.Error(), time.Since(start))
		}
		return health.NewCheckResult(health.StatusUp, "", time.Since(start))
	},
	health.WithReadiness(true),
	health.WithCheckMode(health.CheckModeActive),
	health.WithTimeout(2*time.Second),
	health.WithTags("db", "critical"),
)

The rx.Resolver is the DI resolver, so a check reaches whatever the application registered without capturing it at construction time.

Respect ctx. It carries the check's timeout. A check that ignores it can hold up the synchronous startup pass and every subsequent tick.

Registering

Declare checks in the configuration:

app := rex.New(health.WithHealth(health.NewConfig(
	health.WithCheck(dbCheck),
	health.WithChecks(cacheCheck, queueCheck),
)))

You cannot resolve the registry and register before Run. It does not exist until the extension's OnInitialize runs. Applications used to reach into the container for it; that now resolves nothing.

For wiring that needs more than a check — a route that depends on the state store, say — write a small application extension and do it in its OnInitialize:

type wiring struct{ rex.DefaultExtension; h *health.HealthExtension }

func (w *wiring) OnInitialize(ctx context.Context, r rex.Rex) error {
	w.h.RegisterCheckFunc("queue", checkQueue, health.WithReadiness(true))
	return nil
}

Execution modes

Mode Runs Caches For
CheckModeActive on a ticker, every CheckInterval — writes to the state store critical dependencies that must be monitored continuously
CheckModePassive on demand, when a gate needs it for CacheTTL (30s default) optional or expensive dependencies
CheckModeOnDemand on demand, every time never (CacheTTL forced to 0) checks that must be fresh at the moment of use

Active is the default and the right choice for anything a route hard-depends on: the state is already known when a request arrives, so the gate costs a map read.

Passive trades that for not polling something expensive. The first request after the TTL expires pays for the check.

CheckInterval is 10 seconds by default; WithCheckInterval(0) disables the ticker entirely, leaving only on-demand execution.

Readiness

health.WithReadiness(true)

Readiness checks are the ones /ready runs. Mark a check as readiness when the replica cannot usefully serve without it. A check on an optional cache should not remove the replica from the load balancer.

Every check, readiness or not, appears on /status.

Results

type CheckResult struct {
	Status    Status            // UP | DEGRADED | DOWN | UNKNOWN
	Message   string
	Duration  time.Duration
	Timestamp time.Time
	Metadata  map[string]string
}

Use DEGRADED rather than DOWN for "working, but badly" — high latency, a replica lagging, a cache miss rate that has gone wrong. Status.IsHealthy() is true for UP and DEGRADED, /ready returns 200 for both, and the gate can still be told to require UP with WithMinStatus.

Timeouts

health.WithTimeout(2 * time.Second)

Bounds one execution. Keep it well below CheckInterval, and remember that the synchronous startup pass runs every check once before the listeners bind — so the worst case added to your boot time is roughly the sum of the timeouts of checks that hang.

Tags

health.WithTags("db", "critical")
registry.ExecuteByTags(ctx, "critical")

Useful for a subset probe or an operator endpoint that exercises one group.

Checks that mirror dependency state

If something else already reports into the state store — a wrapped HTTP client, your own ReportFailure calls — you do not need a polling check. Expose the state as a check instead:

health.NewDependencyCheck("payments", "payments-api", stateStore, true, "external")

It reads the store rather than performing an operation, so it costs nothing and reflects real traffic instead of a synthetic ping.

The registry

ext.Registry() gives you the live registry:

Register / Unregister / Get / GetAll
GetByTags / GetReadinessChecks / GetActiveChecks / GetPassiveChecks
ExecuteAll / ExecuteReadiness / ExecuteByTags / ExecuteCheck
Start(interval, stateStore) / Stop
SetResolver / SetLogger

The extension calls Start and Stop for you around the application lifecycle.

Clone this wiki locally