Chio/Docs
LOGIN · JOIN

Health & Readiness

Choose the health check for the service you run, keep a node in rotation only while it can record, and diagnose refusals.

Deployment chio@5f63e6e

On this page

Each Chio service reports at its health routes whether it can record what it decides, and the kernel tests the same conditions again before every dispatch. Keep a node in rotation only while both hold.

Liveness, readiness, and the kernel’s pre-dispatch gates each read a different part of the node. A process can answer and still refuse work, so an operator reads all three after a start, a recovery, or a refusal.

Start
Health signalsChoose an asker.
r1/chio/liver2/chio/healthagoverned calldtoolrefusedKpprocessρreceipt storegcommit writergtrusted locksκauthorityπspec and policyr1/chio/liver2/chio/healthagoverned calldtoolrefusedKpprocessρreceipt storegcommit writergtrusted locksκauthorityπspec and policy

Each check reads a different part of the node

Liveness asks whether the process answers. Readiness asks whether the node can record and admit. The kernel’s own pre-dispatch gates decide each governed call. Choose a check.

One sidecar node and the three askers that reach it. Admin and trust-control health routes, metrics, and the watchdog that samples the commit writer are left out.

1. Choose the check that matches the service

Each service exposes its own routes. Probe the ones that belong to the service you deployed:

ServiceCheckWhat it tells you
chio api protectGET /chio/liveThe process answers, without probing storage.
chio api protectGET /chio/healthThe node can record and admit; 503 when it cannot.
chio mcp serve-httpGET /admin/healthAuthority, configuration, and sessions, behind an admin token.
chio trust serveGET /health and GET /v1/authorityAuthority availability and peer sync staleness.

/chio/live and /chio/health exist only on the API-protect sidecar. On trust-control, read the nested fields: the body can say ok: true while authority.available is false. The MCP edge summary in the Admin API and trust-control’s configuration booleans report configuration; the sidecar readiness probe is the check that tests a receipt-store write.

2. Read one health response

With the sidecar example running on localhost, request its readiness route:

api-protect · healthtranscript
$ curl -s http://127.0.0.1:9090/chio/health
{"status":"healthy","version":"0.1.0","receipt_backend":"durable","revocation_backend":"durable"}
exit 0

You should see healthy, the version, and durable receipt and revocation backends. Liveness answers 200 with those backend fields empty, because it inspects nothing past the process. One response covers one moment; the probes in step 3 repeat it. For the MCP edge, use a separate admin credential and the captured admin health request.

3. Let readiness pull an instance that cannot record

/chio/health returns 503 when any of four dependencies fails:

  • Receipt store. A reserved row inserted into the receipt tables and rolled back fails, catching a missing table, read-only storage, or a full disk without leaving a probe receipt.
  • Commit writer. The writer’s health flag has left Healthy.
  • Capability authority. The authority the sidecar checks grants against is unreachable.
  • Spec and policy. The OpenAPI spec or the policy did not load.

The status is Degraded or Unhealthy, both 503. Chio returns the code; the orchestrator removes the instance, and only where a readiness probe is configured. A startup probe alone never checks again once the gate opens. The reference manifests probe these routes:

ProbeRouteReference manifests
Startup/chio/healthCloud Run service.yaml, Azure container-app.bicep
Readiness/chio/healthCloud Run, Azure
Liveness/chio/liveCloud Run, Azure, the sidecar image HEALTHCHECK
ECS healthCheck/chio/healthdeploy/ecs/task-definition.json, 15s start period

Read more in the readiness probe reference.

A critical flag closes the kernel’s door before dispatch

The kernel refuses every governed call while a flag marked tcb_critical is above Healthy. There is no grace band:

crates/core/chio-supervisor/src/health.rsrust
/// True when the surface must fail closed: TCB-critical and not
/// [`HealthLevel::Healthy`]. Read on the pre-dispatch path.
#[must_use]
pub fn is_serving_closed(&self) -> bool {
    self.0.tcb_critical && !matches!(self.level(), HealthLevel::Healthy)
}

Two flags carry it. The trusted-lock flag trips on the first poisoned lock guarding multi-field state in the budget registry, session state, or the receipt logs, and ensure_tcb_locks_healthy then refuses each evaluation; continuing on half-mutated budget state would corrupt monetary accounting. The receipt commit writer’s flag trips after one fault, and ensure_receipt_persistence_ready refuses before dispatch, because the kernel writes a receipt after the tool runs and a dead writer found afterwards means an effect with no durable evidence.

Keep the node out of rotation while you investigate. Recovery restores the dependency the gate protects; lowering a flag over a damaged store or poisoned state would admit calls nothing can record.

Read more in the writer conditions.

A tripped worker stays tripped until it is cleared

Every supervised worker carries a HealthFlag from chio-supervisor. Failures raise it; a later success resets the failure count and leaves the level where it is.

Health flagChoose a transition.
trip_after failuresmax_restarts spentspawn failurerecord_okclear, the only way downHealthyDegradedFailedserving closed when tcb_criticaltrip_afterbudgetspawn failurerecord_okclearHealthyDegradedFailedclosed if tcb_critical

Failures only raise a flag

A supervised worker carries a HealthFlag. Failures raise it from Healthy toward Failed, and raising is a compare-and-swap, so a concurrent escalation is never lost behind a lower one. A flag built tcb_critical reports serving closed at any level above Healthy. Critical flags.

One flag’s three levels and every transition between them. Thresholds are per supervisor; the receipt commit writer trips after one failure and allows five restarts. Timing, backoff, and the reason string are left out.

A recovered thread therefore cannot erase its own incident. The receipt commit writer, the trust-control cluster sync loop, and the SIEM exporter each run under a supervisor with their own flag, and each flag reaches the node’s metrics as chio_task_health and chio_task_restart_total.

Read more in the health flag fields and the supervisor configuration.

A stalled writer refuses dispatch as surely as a dead one

A writer thread can be alive and making no progress. The receipt store classifies its commit actor from in-memory counters, and the kernel gates on the result:

ReceiptWriterLivenessClassified whenDispatch
DeadThe commit-actor channel was found disconnectedRefused
WedgedA backlog made no progress for 10,000 msRefused
SaturatedThe channel is at capacityRefused
HealthyNone of the aboveAdmitted
UnknownNo store, no async writer, or an outside observerAdmitted

A watchdog publishes the verdict every 1,000 ms; without one, the kernel samples the store inline on each pre-dispatch check, so a wedged writer still refuses. Unknown passes this check only, and the separate durability gate still requires a configured sink unless ephemeral operation was explicitly enabled.

Terminalbash
chio --receipt-db /var/lib/chio/receipts.sqlite3 receipt health --json

This command opens the database read-only from outside the serving process. It reports persisted state, and its writerLiveness reads unknown because it cannot see the server’s queue. Read writer health from the serving node.

Read more in the receipt health command.

Recover by fixing the cause, then restarting

The supervisor retries within its failure threshold, restart budget, and backoff, and a spent budget leaves the worker at Failed. Recover in this order:

  1. Find the cause. Read the flag’s reason, chio_task_restart_total, and chio_lock_poison_total; on trust-control, read chio_cluster_sync_staleness_seconds.
  2. Fix it. Repair the storage, resource, or worker fault. A writer whose verified head was poisoned by a store-wide append fault needs its head reseeded.
  3. Restart under control. Stop the node as Node Lifecycle describes; the new process starts with fresh flags.
  4. Prove it. Confirm /chio/health returns 200, then send one governed request and read its receipt before restoring traffic.

A responding process, a passing readiness probe, a healthy writer, and a governed call with its receipt establish four different facts. Accept the node only when all four hold.

Health flags, readiness, and supervision

Readiness probe

The receipt check is not a connection ping. A bare SELECT 1 still answers over dropped tables, a read-only mount, or a full disk, which would hold an instance in rotation while every append fails after an allowed upstream call. The probe inserts into http_receipts and tool_receipts under the reserved key __chio_readiness_probe__ inside a transaction that is always rolled back. reachability_probe_touches_the_write_path_and_persists_nothing drops http_receipts out of band and asserts readiness fails.

The handler maps Healthy to 200 and Degraded or Unhealthy to 503, alongside the version and the embedded kernel’s receipt_backend and revocation_backend. A sidecar with no receipt_db has no receipt store to fail, so its receipt check always passes.

Rotation contract

A node whose receipt store can no longer accept a write, whose commit writer has tripped, or whose authority or policy is unavailable returns 503 from /chio/health, and a platform with a readiness probe pulls it from routing instead of sending it traffic it can only refuse. The contract covers one sidecar process. readiness_status and sidecar_health_handler implement it; the readiness probes in the Cloud Run and Azure reference manifests and the ECS container healthCheck act on it.

Writer conditions

writer_serving_closed() is true when the supervised thread’s flag has left Healthy, when the store has no writer handle, or when the writer’s verified head is poisoned. The head starts poisoned: ReceiptCommitWriterHealth::default sets it, and the actor clears it only after it seeds a head, so a store still attaching cannot admit a governed action before its first append could reject. A store-wide append fault poisons it again, and every append is rejected until an operator reseeds. The thread flag alone cannot see this, because the thread is alive.

The kernel holds one HealthFlag::new(true) for trusted state. record_tcb_lock_poison records a failure with trip_after = 0 and increments chio_lock_poison_total. The receipt store supervises its writer with tcb_critical: true and trip_after: 1.

Liveness classification

Dead is classified when the writer’s last recorded error contains unavailable; the store writes sqlite receipt commit actor is unavailable when it finds the channel disconnected at enqueue. Wedged covers a caller-timed-out command still owned by the actor, or a backlog with no progress for receipt_writer_stall_ms (default 10,000). The stall clock anchors to the later of the last commit and the current backlog’s start, so an idle writer is not judged wedged the instant work arrives; with neither anchor set it is never stalled. The watchdog polls every receipt_writer_poll_ms (default 1,000).

Health flag fields

HealthFlag lives in crates/core/chio-supervisor, a leaf crate with no other Chio dependency. It is a cloneable handle over shared atomics, so level and counter reads are lock-free on the pre-dispatch path; only the reason string sits behind a mutex.

FieldTypeMeaning
level
HealthLevel
Severity, raised monotonically.
tcb_critical
bool
Fixed at construction. Marks a worker whose degradation fails evaluations closed.
consecutive_failures
u32
Reset by a completed unit of work; compared with trip_after and max_restarts.
restart_total
u64
Every recorded failure, including a thread that could not be spawned. Survives clear; a new process starts a new count.
last_ok_unix_ms
u64
Stamp from the last completed unit of work. Zero renders as None.
last_transition_unix_ms
u64
When the level last changed, in either direction.
reason
Option<String>
Last failure text, behind a mutex whose only invariant is the single Option.

HealthFlag::snapshot() renders these as a serializable HealthSnapshot: camelCase field names, snake_case level values such as "degraded", optional fields #[serde(default)]. It is an operator payload, not a signed record. The SQLite receipt store lifts level and restart_total into writerLevel and writerRestartTotal on its health report, and the SIEM exporter’s ExporterManager::health() embeds the whole snapshot.

Level transitions

LevelReached whenSupervisor state
Healthy
Initial state, or after an explicit clear.
Serving normally.
Degraded
Consecutive restart-worthy failures reach trip_after; with trip_after == 0, the first failure.
Restarts may still be running.
Failed
The restart budget is spent, or the thread could not be spawned.
Terminal: respawning stops and the flag stays set to be read.
crates/core/chio-supervisor/src/health.rsrust
/// Record a completed unit of work. Resets the consecutive-failure counter and
/// stamps liveness, but never lowers a tripped level.
pub fn record_ok(&self, now_ms: u64) {
    self.0.consecutive_failures.store(0, Ordering::SeqCst);
    self.0.last_ok_unix_ms.store(now_ms, Ordering::SeqCst);
}

Raising is a compare-and-swap loop, so a concurrent escalation to Failed is never lost behind a lower raise, and clear is the only path back to Healthy. The unit suite pins each edge (record_ok_resets_consecutive_failures_without_lowering_level, raise_is_monotonic_and_ignores_lower_severity, clear_is_the_only_downgrade_and_preserves_restart_total), and a property test over random operation sequences in tests/health_properties.rs asserts the level never decreases between clears while restart_total equals the number of recorded failures. A loom model in tests/loom_health.rs, run nightly behind --cfg loom, checks one worker recording a failure against one concurrent reader for a torn or invalid level; it is a two-thread interleaving check of the flag.

Supervisor configuration

SupervisorConfig carries the policy, every field public. SupervisorConfig::new(name, tcb_critical) supplies the convention below; the receipt commit writer builds the struct field by field.

Field::new conventionReceipt commit writerEffect
trip_after
3
1
Consecutive restarts before Degraded.
max_restarts
10
5
Consecutive restarts before terminal Failed.
base_backoff
100ms
100ms
Delay before the first re-entry.
max_backoff
60s
30s
Ceiling on the doubling.

The delay for the count-th consecutive restart is min(max_backoff, base_backoff * 2^(count - 1)), computed with checked_shl and saturating multiplication. An iteration that completes returns SupervisedOutcome::Continue and resets the consecutive counter, so isolated faults separated by healthy work never accumulate into a trip. The release profile sets panic = "unwind", so the catch_unwind restart path is live in release builds.

The async supervisor, SupervisedTask behind the async feature, runs each iteration in its own child task, because catch_unwind does not capture a panic across an .await cleanly. Trust-control’s cluster sync loop and the SIEM exporter run under it, the first reporting peer staleness on trust-control’s /health.

Receipt health command

chio receipt health samples the database through a separate read-only connection and rejects --control-url. It cannot see the serving process’s supervisor, so it prints writer_level: healthy with zeroed writer counters and serializes writerLiveness: "unknown" whatever that process is doing. The --json form, schema chio.cli.receipt.health.v1, carries writerLevel beside writerLiveness; the human form omits the liveness field.

Next: stop and replace a node safely