Skip to content

Health and readiness

Health checks let liveness and readiness probes observe dependency state. Arqen provides a registry-based system with parallel execution, timeouts, and degraded states.

Concepts

HealthStatus

Every check returns one of three states:

StatusHTTP codeMeaning
Healthy200Dependency is operating normally
Degraded { reason }200Dependency is functional but impaired
Unhealthy { reason }503Dependency is not functioning

Liveness vs readiness

  • Liveness (/health): Is the application alive? Runs all registered checks. Used by orchestrators to decide whether to restart the process.
  • Readiness (/ready): Is the application ready to serve traffic? Runs only checks where required_for_readiness() returns true. Used to decide whether to route traffic to the instance.

Types

HealthCheck trait

rust
#[async_trait]
pub trait HealthCheck: Send + Sync {
    fn name(&self) -> &str;
    async fn check(&self) -> HealthStatus;
    fn timeout(&self) -> Duration { Duration::from_secs(5) }
    fn required_for_readiness(&self) -> bool { true }
}

Implement this trait for each dependency you want to monitor. The timeout method defaults to 5 seconds. Set required_for_readiness() to false for non-critical checks.

HealthRegistry

Collects health checks and runs them in parallel:

rust
use arqen::health::{HealthRegistry, AlwaysHealthy};
use std::sync::Arc;

let mut registry = HealthRegistry::new();
registry.register(Arc::new(AlwaysHealthy));

let report = registry.check_liveness().await;
// report.status == HealthStatus::Healthy
// report.checks.len() == 1

HealthReport

The result of running all checks:

rust
pub struct HealthReport {
    pub status: HealthStatus,
    pub checks: Vec<CheckResult>,
    pub timestamp: String,
    pub probe_type: ProbeType, // Liveness or Readiness
}

ModuleHealth

Module-level health status that converts into HealthStatus:

rust
pub enum ModuleHealth {
    Healthy,
    Degraded { reason: String },
    Unhealthy { reason: String },
}

Modules return this from health_check(). It is automatically registered with the health registry when ModuleBuilder::register_all() is called.

HTTP endpoints

EndpointProbe typeBehavior
GET /healthLivenessRuns all checks, returns 200 or 503
GET /readyReadinessRuns required checks, returns 200 or 503

Both endpoints return JSON:

json
{
  "status": "healthy",
  "checks": [
    {
      "name": "database",
      "status": "healthy",
      "duration_ms": 2
    }
  ],
  "timestamp": "2026-08-05T12:00:00Z",
  "probe_type": "liveness"
}

Registering checks

From a module

Use ModuleContext in Module::register() to register checks:

rust
fn register(&self, ctx: &mut ModuleContext<'_>) -> Result<(), AppError> {
    ctx.health.register(Arc::new(MyDatabaseCheck { /* ... */ }));
    Ok(())
}

Module health checks are also auto-registered from the health_check() method.

Directly on the registry

rust
let mut registry = HealthRegistry::new();
registry.register(Arc::new(DatabaseCheck::new("postgres://...")));
registry.register(Arc::new(RedisCheck::new("redis://...")));
registry.register(Arc::new(ExternalApiCheck::new("https://api.example.com")));

Code example

From the health module tests (health.rs):

rust
use arqen::health::{HealthRegistry, AlwaysHealthy, AlwaysDegraded, OptionalCheck};
use std::sync::Arc;

#[tokio::test]
async fn test_readiness_skips_optional() {
    let mut registry = HealthRegistry::new();
    registry.register(Arc::new(AlwaysHealthy));
    registry.register(Arc::new(OptionalCheck));

    let liveness = registry.check_liveness().await;
    assert_eq!(liveness.checks.len(), 2); // runs all

    let readiness = registry.check_readiness().await;
    assert_eq!(readiness.checks.len(), 1); // skips optional
}

See: crates/arqen/src/health.rs

Rust-first implementation · language-agnostic application positioning