Status: built (V1). Enable with
HYPHAEDB_HA_ENABLED=1 on both instances (the Helm chart
sets it automatically when replicaCount > 1). One instance wins the lease and serves; the other
runs as a wait-then-boot standby — it holds no mesh, issues zero PostgreSQL writes, answers
/healthz 200 and /readyz 503, and promotes automatically (running the ordinary primary boot)
when the primary dies or drains. A planned drain hands over within ~one poll interval; a crash
failover waits out the fence-before-promote margin (~8s default) plus the promotion boot. Watch
hyphae_health_role (0 standby, 1 primary, 2 fenced) and see the failover runbook in
deploy/runbooks/ha-failover.md.emptyDir snapshot cache, which bounds same-pod restarts but not a different pod’s
promotion (that pod’s cache is empty, so it pays the full PostgreSQL scan) — run a StatefulSet
with per-pod persistent volumes if cross-pod failover time matters at your corpus size. Prefer the
TEI embedding sidecar, which warms during the standby wait.
This works because PostgreSQL is the single source of truth and everything in the process — the HNSW
graph, trust scores, agent positions, in-flight gossip — is reconstructable from it. Failover is a
rebuild, not a live-state handoff.
The lease
Leader election rides on PostgreSQL, with no new etcd or ZooKeeper dependency. The lease is a PostgreSQL advisory lock (pg_try_advisory_lock), backed by an ha_lease heartbeat row:
- The primary holds the advisory lock and updates
renewed_ateverylease_renew_interval = 2s. - The advisory lock is session-scoped: if the primary’s connection dies, PostgreSQL releases the lock automatically — no stale lock survives a dead holder.
- A holder is live only while
now() - renewed_at < lease_ttl = 6s. Past that, the lease is expired and eligible for takeover.
Fence before promote
Split-brain prevention is structural, not heuristic. A single advisory lock is mutually exclusive by construction — at most one session holds it. The standby cannot promote until it acquires the lock, and it can only acquire the lock once the old holder’s session has ended. The timing ordering closes the time-based race:Failover sequence
1
Detect
The standby polls
ha_lease.renewed_at. Failover becomes a candidate when the lease has been
expired (now() - renewed_at ≥ lease_ttl).2
Wait the promote delay
The standby waits until the lease has been expired for
standby_promote_delay = 8s, guaranteeing
the old primary’s self-fence deadline has elapsed.3
Acquire the lock (fence)
pg_try_advisory_lock succeeds only if the old holder’s session is gone. Failure means another
standby won or the old primary recovered — abort and resume polling.4
Restore the mesh
Load the latest snapshot and replay only the rows newer than its watermark — a bounded delta, not
a full table scan. See /operations/durability-and-dr.
5
Rebuild trust and start workers
Re-hydrate agent trust scores from storage — trust is written through to PostgreSQL on every
update, so the new primary reads the exact accrued scores, not a cold reset. Then start the
gossip, promotion, and sweep workers and begin renewing the lease.
6
Flip readiness
Once the
"graph" probe reports Up, flip /readyz to ready and accept RPCs as the new primary.Client reconnection
Failover is indistinguishable from a single-server restart, so the SDKs need no HA-specific code. The client sees its stream error, backs off, reconnects to whichever process is primary (the load balancer routes only to a ready one), re-subscribes, and callsget_inbox(since = last_seen) to backfill. This
works because delivery watermarks are PostgreSQL-sourced and therefore valid across instances.
RPO and RTO
/readyz gates load-balancer membership, so traffic routes only to a ready primary. A standby holds
no live mesh and issues zero writes until it is promoted.