
ctx Hub: High-Availability Cluster¶
Run multiple hub nodes with Raft-based leader election for redundancy. Any follower can take over if the leader dies.
This recipe assumes you've read the
ctx Hub overview and the
Multi-machine setup. HA only makes
sense in the "small trusted team" story; a personal
cross-project brain on one workstation does not need three Raft
peers.
Raft-Lite
ctx uses Raft only for leader election, not for data
consensus. Entry replication happens via sequence-based gRPC
sync on the append-only JSONL store. This is simpler than full
Raft log replication and is possible because the store is
append-only and clients are idempotent. The implication:
a write accepted by the leader is durable on the leader
immediately; followers catch up asynchronously. If the leader
crashes between accepting a write and replicating it,
that write can be lost. Do not use the hub as a bank ledger.
Topology¶
A minimum HA cluster is three nodes. Two is worse than one: it doubles failure probability without providing quorum.
+-------------+
| client(s) |
+------+------+
|
+-----------+-----------+
| | |
+---v---+ +---v---+ +---v---+
| hub A | | hub B | | hub C |
| :9900 | | :9900 | | :9900 | gRPC (clients, data sync)
| :9901 | | :9901 | | :9901 | Raft (leader election)
+-------+ +-------+ +-------+
^ ^ ^
+-----------+-----------+
Raft (leader election)
gRPC (data sync)
Each node runs two listeners: the hub's gRPC port that clients
dial (--port), and the Raft port the other nodes dial
(--raft-bind). They are separate addresses; the peer list is
made of Raft addresses.
Step 1: Bootstrap the First Node¶
ctx hub start --daemon \
--port 9900 \
--raft-bind hub-a.lan:9901 \
--peers hub-b.lan:9901,hub-c.lan:9901
--raft-bind is the address this node advertises to the other
two, so it has to be a host they can dial: a bare port
(:9901) or a wildcard (0.0.0.0:9901) is rejected at
startup. Every node's --raft-bind appears in the other nodes'
--peers lists, and each node bootstraps that same set.
The node starts a Raft election as soon as it sees its peers.
Until a quorum answers, ctx hub status reports
Leader: unknown (election in progress) — expected while the
other nodes are still coming up.
Step 2: Start the Other Nodes¶
On hub-b.lan:
ctx hub start --daemon \
--port 9900 \
--raft-bind hub-b.lan:9901 \
--peers hub-a.lan:9901,hub-c.lan:9901
On hub-c.lan:
ctx hub start --daemon \
--port 9900 \
--raft-bind hub-c.lan:9901 \
--peers hub-a.lan:9901,hub-b.lan:9901
After a few seconds, one node wins the election and becomes the leader. The other two are followers.
Step 3: Verify Cluster State¶
From any node:
Expected output on the node that won the election:
and on either of the others:
The leader is named by its Raft address, which is what the cluster agrees on. Clients still dial the hub port.
Peers: counts the servers in the committed Raft configuration
other than the one answering, so a three-node cluster reports
two from every node. If the line reads
Leader: unknown (election in progress), Raft has no leader for
the current term: either the election is still running, or the
node you asked cannot see a quorum.
Step 4: Register Clients with Failover Peers¶
The ctx hub * commands above run on the hub nodes themselves and
don't need a project. The ctx connection * commands below are
different: they live inside a project (the encrypted hub config is
stored at .context/.connect.enc), so you have to tell ctx which
project first.
When registering a client, give it the full peer list:
# In the project directory on the client:
ctx connection register hub-a.lan:9900 \
--token ctx_adm_... \
--peers hub-b.lan:9900,hub-c.lan:9900
If the leader becomes unreachable, the client reconnects to the next peer. Followers redirect to the current leader, so writes always land on the right node.
Runtime Membership Changes¶
Membership changes are admin-gated and leader-only: pass
--token (or set CTX_HUB_ADMIN_TOKEN) and run them against
the leader that ctx hub status names. A follower answers
not the leader rather than pretending.
Adding a node is two steps, because a new node must not bootstrap a configuration of its own — it waits for one:
# On hub-d.lan, the new node:
ctx hub start --daemon \
--port 9900 \
--raft-bind hub-d.lan:9901 \
--join
# On the leader:
ctx hub peer add hub-d.lan:9901 --token ctx_adm_...
ctx hub status on hub-d.lan then reports Role: Follower
and names the leader; every node's Peers: count goes up by
one.
Remove a decommissioned peer, again on the leader:
Removal shrinks the quorum, which is the point: a node you have taken away should stop counting against liveness. Removing one of three leaves two, and a two-node cluster needs both to be up — plan the next addition accordingly.
Planned Maintenance¶
Before taking a leader offline, hand off leadership:
stepdown asks Raft to transfer leadership to a follower that
is caught up, and returns once the transfer completes. Run
ctx hub status afterwards to see which node won. Then stop
the old leader; the cluster keeps serving throughout, because
the handoff happened before the process went away.
Failure Modes at a Glance¶
| Event | What happens |
|---|---|
| Leader crashes | New election; clients reconnect to new leader |
| Follower crashes | No write impact; catches up on restart |
| Network partition (majority) | Majority side keeps serving; minority read-only |
| Network partition (split) | No quorum; all nodes read-only |
| Disk full on leader | Writes rejected; read traffic continues |
For the full list, see Hub failure modes.
See Also¶
- Multi-machine recipe: single-node deployment
- Hub operations: backup and maintenance
- Hub security model: TLS, tokens