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:
| Status | HTTP code | Meaning |
|---|---|---|
Healthy | 200 | Dependency is operating normally |
Degraded { reason } | 200 | Dependency is functional but impaired |
Unhealthy { reason } | 503 | Dependency 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 whererequired_for_readiness()returnstrue. Used to decide whether to route traffic to the instance.
Types
HealthCheck trait
#[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:
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() == 1HealthReport
The result of running all checks:
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:
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
| Endpoint | Probe type | Behavior |
|---|---|---|
GET /health | Liveness | Runs all checks, returns 200 or 503 |
GET /ready | Readiness | Runs required checks, returns 200 or 503 |
Both endpoints return 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:
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
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):
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
}