VeltrixDB Docs
v1.16 GitHub FAQ Quickstart

Deployment Modes

VeltrixDB serves every request from local state by default. The distributed layer is opt-in via the -mode flag and is wired directly into the serving path: every mutating request — plain KV (PUT/DELETE/MultiPut), atomics, TXN, and (since v1.1.0) namespace, hash-field, vector, secondary-index, and list/set writes — passes through a write coordinator in front of the storage engine, on both the text and binary protocols.

The three modes

Set the mode with -mode standalone|raft|replicated; raft and replicated are mutually exclusive (one coordinator per process, never both). Here is exactly what each mode guarantees for writes and reads — this is the load-bearing reference for choosing a mode, so read it precisely rather than skimming.

-modeHow writes are handledConsistency guarantee
standalone default Straight to the local engine — byte-for-byte the pre-existing single-node path. No Raft, no replication, no redirects. Single-node linearizable (one writer, one copy).
raft Ops are gob-encoded and submitted to a Raft log, committed by quorum, and applied on every node via a storage-backed finite state machine. Non-leaders reject writes with a MOVED <leader-addr> <leader-id> redirect. Linearizable writes (single Raft group, quorum commit). Reads default to local applied state — fast but possibly stale on followers; a newly elected leader holds GET / MGET until its no-op applies (v1.12). With --linearizable-reads, GET and MGET run the ReadIndex fence: quorum-confirmed, never stale, one heartbeat round-trip per read. Fixed in v1.14 both cover every read command, not only GET / MGET.
replicated The write is applied to the local engine first, then handed to the replication engine. The -consistency flag decides when the client is ACKed. Primary-copy durability across N copies; NOT linearizable under concurrent writers (no single-writer ordering). Reads are local.
Network front-end is standalone-only New in v1.7 The opt-in C++ network front-end (--net=cpp|uring|poll) writes straight to the local engine, bypassing Raft/replication routing, so the server refuses to start with it in --mode=raft or --mode=replicated (and with --auth-config). Cluster modes always use the default --net=go. See Wire Protocol for the commands each front-end serves.

The --consistency flag (replicated mode)

-consistency only applies in replicated mode and controls the point at which the client's write is acknowledged:

ValueACK semanticsError behavior
eventual defaultAsync — ACKs immediately after the local write; replicas catch up in the background.No blocking wait; a replica that is down falls behind — once a send to it fails it is marked FAILED (on v1.13 and earlier skipped until the primary restarts; since v1.14 re-probed and caught up, see Replication & CDC).
quorumACKs only after a majority of replicas (counting the local copy) have applied the write.Surfaces ErrQuorumNotReached or ErrReplicationTimeout (10 s) if a majority cannot be reached.
strongACKs only after all replicas have applied the write.Surfaces ErrReplicationTimeout (or ErrQuorumNotReached) if the required copies cannot be reached — for example, a strong write with a majority of replicas down errors instead of silently ACKing.

Linearizable reads: --linearizable-reads (raft mode)

By default, raft-mode reads are served from whatever state the receiving node has locally applied — fast, but possibly stale (a follower may lag; a partitioned ex-leader can serve reads after a new leader has committed writes elsewhere). New in v1.12 A freshly elected leader waits for its term's no-op to apply before serving GET / MGET (WaitLeaderApplied, ≤ 2 s), so reads on the leader include every write its predecessor acknowledged. Starting --linearizable-reads (default false) closes the remaining hole by running every GET and MGET through the Raft §6.4 ReadIndex fence, without writing anything to the log:

  1. Capture the fence — the leader records its current commitIndex as the read fence. The fence is only valid once the leader has committed an entry in its own term; immediately after an election it returns a retryable "leadership not yet established" error until the election no-op commits.
  2. Quorum heartbeat round — the leader sends a fresh heartbeat to its peers and waits for a quorum of acknowledgements, proving no higher-term leader existed at that moment.
  3. Apply catch-up — the read waits until the local state machine has applied up to the fence, then reads from the engine.

Semantics and cost: reads are quorum-confirmed and never stale, at the price of one heartbeat round-trip per read (plus any apply catch-up). Only the leader can serve them — followers reject reads with a MOVED <leader-addr> <leader-id> redirect, exactly like writes, which the cluster-aware client follows transparently. Each fence is bounded by a 2-second timeout covering the heartbeat round and apply catch-up. The flag has no effect outside --mode=raft (the server logs a warning and ignores it).

v1.13 and earlier: only GET and MGET are covered On v1.13 and earlier both the new-leader barrier and the ReadIndex fence apply to GET and MGET only. Namespace, hash-field, list/set and RANGE / SCANCUR reads skip them and stay local, even with --linearizable-reads. Fixed in v1.14 every read on both protocols passes them — namespace, hash, list and set reads, RANGE / SCANCUR, VER / GETVER, IDXQUERY, QUERY and all searches — so with --linearizable-reads followers answer MOVED to all of them.

Automatic rebalancing: --auto-rebalance

In replicated mode, membership events — a node added to or removed from the membership — trigger a ring rebalance plus physical key migration. Node state changes (failed, recovering, active) never start a migration, and in raft mode migration never runs at all (every node holds the full state; -auto-rebalance is a logged no-op there). On v1.14 and earlier a state change also started a migration, which could delete committed keys — run those releases with -auto-rebalance=false. A rebalancer subscribes to partition-map membership events, debounces bursts of gossip (3-second window), then recomputes partition ownership and hands off to the TransferAgent, which streams re-owned keys to their new owners over a dedicated partition-transfer HTTP listener on client port +5 (override with -transfer-addr). Local copies are deleted only after the destination confirms receipt, so an interrupted migration is simply retried on the next membership event — it re-sends the unconfirmed remainder. Each batch carries the sender's membership epoch and a receiver with a newer one refuses it (HTTP 409). Vector-namespace settings (@vecns/) and index definitions (@idxdef/) are copied to every node rather than moved. When the cluster TLS flags are set, transfer traffic uses the same inter-node TLS/mTLS configuration.

v1.14 and earlier: a node failure can delete keys On v1.14 and earlier the migration above also runs when a node is marked FAILED, in raft mode too: every node sends each key to its single ring owner and deletes it locally, ignoring the replication factor. Killing the leader of a loaded 3-node raft cluster (20,000 keys) left 5,435 and 7,875 keys on the survivors. Run clusters with --auto-rebalance=false until you upgrade. Fixed in v1.15 in --mode=raft migration never runs (--auto-rebalance is a logged no-op and /transfer/keys answers HTTP 403; every node applies the whole log); node failure or recovery never starts a migration in either mode; and in --mode=replicated a key stays on every node that is one of its RF replicas, is copied to replicas that newly need it, and is deleted from a node only after every current replica acknowledged it. See Cluster & Consensus.

On v1.16 and earlier membership is static. Each server registers the nodes in --peers (or --seeds) at startup; there is no runtime join or drain command and gossip does not carry membership, so adding a node means restarting every node with it in --peers. New on main after v1.16 — ships in the next release nodes are added and removed online — see the membership runbook below; in replicated mode an online add or remove is what triggers the migration.

Signing node-to-node traffic: --cluster-secret-file New in v1.11

The transfer listener carries both key migration (/transfer/keys) and distributed search (/internal/search). Put the same secret (at least 16 bytes) on every node with --cluster-secret-file or VELTRIXDB_CLUSTER_SECRET: every request is then signed with HMAC-SHA256 over time, method, path and body, and refused (HTTP 401) if it does not verify or is more than 5 minutes off the receiver's clock. Without a secret or --cluster-mtls, the listener is unauthenticated and the server logs a warning at startup.

Search in cluster modes

Vector, text and hybrid searches, QUERY and IDXQUERY run on every non-failed node and are merged (--search-fanout, default true). The request fails closed: a peer that misses --search-timeout-ms (2000) fails it with an error naming the peer, unless --search-allow-partial is set. See Cluster & Consensus and Vector & Hybrid Search.

Auto-rebalance is on by default; disable it with --auto-rebalance=false if you want membership changes to update routing only, with data migration under manual control (the transfer listener still starts so other nodes can push keys to this one). Fixed in v1.15 it has no effect in --mode=raft.

Rack-aware placement (v1.1.0): start each node with --rack-id <zone> (e.g. --rack-id=asia-south1-a) and rebalance never places two copies of a partition in the same failure domain — unless the replication factor exceeds the number of distinct racks, in which case it falls back to distinct nodes while still spanning every rack available. Rack labels spread between members via gossip digests, so only the local node needs the flag, and /admin/cluster reports each node's "rack". Rackless clusters keep the exact previous deterministic round-robin placement. See Cluster & Consensus.

Launching a 3-node Raft cluster

Run the same command on each host with an identical -peers value, changing only -node-id and -addr:

# 3-node raft cluster (run on each host, same --peers value)
veltrixdb -mode raft -node-id n1 -addr 127.0.0.1:9000 \
  -peers n1@127.0.0.1:9000,n2@127.0.0.1:9100,n3@127.0.0.1:9200

Repeat on the other two hosts with -node-id n2 / -addr 127.0.0.1:9100 and -node-id n3 / -addr 127.0.0.1:9200 respectively. Each peer entry is id@host:port, where host:port is that peer's own -addr (its client-facing storage port) — inter-node Raft, replication, gossip, and partition-transfer listeners are derived automatically from that port (by default, client port +2 / +1 / +3 / +5 respectively) unless overridden with -raft-addr, -repl-addr, -gossip-addr, or -transfer-addr.

Adding and removing nodes online New on main after v1.16 — ships in the next release

On v1.16 and earlier the only way to change membership is to restart every node with a new --peers list. On main, every membership endpoint is on the admin listener with the same auth as the rest of /admin/* (--admin-token, else loopback only):

RequestCLIDoes
GET /admin/cluster/membersveltrix cluster membersMembers, roles (voter / learner in raft, member in replicated), version, leader or coordinator, this node's status (voter, learner, member, joining, draining, removed)
POST /admin/cluster/members {"id":"n4","addr":"10.0.0.14:9000","wait_ms":60000}veltrix cluster add-node n4@10.0.0.14:9000Add a node; addr is its client --addr
DELETE /admin/cluster/members/n2[?force=true&wait_ms=N]veltrix cluster remove-node n2 [--force]Remove a node

Answers: 200 done; 202 raft learner added but not promoted yet (poll members); 400 bad request or last voter; 404 unknown id; 409 duplicate id, a change already in progress (retry), a concurrent replicated change, or a failed drain; 503 no reachable quorum, no leader or coordinator unreachable. Any node accepts a request and forwards it. Forwarding, --join and every replicated-mode change are node-to-node traffic and need --cluster-secret-file (or --cluster-mtls) on every node; without one, a raft change still works when sent to the leader's admin endpoint, nothing else does. Change one node at a time.

Raft: add a node

  1. Start the new node empty with --join:
    veltrixdb --mode=raft --node-id=n4 --addr=10.0.0.14:9000 --data=/data \
      --join=10.0.0.11:9000,10.0.0.12:9000 --cluster-secret-file=/etc/veltrix/secret
    (add --advertise-addr=host:port when --addr has no host, e.g. :9000). It never campaigns or votes until a leader adds it, asks the listed members to add it, and retries every 2 s.
  2. The leader adds it as a learner (log entries or a chunked InstallSnapshot; never counted toward a quorum), then promotes it to voter once a catch-up round ends within 1 s.
  3. Without a cluster secret, add it explicitly: veltrix --addr <leader-admin> cluster add-node n4@10.0.0.14:9000.
  4. Check: veltrix cluster members on any node lists n4 voter. Raft mode moves no keys: the new node gets everything through the log.

Raft: remove a node

veltrix cluster remove-node n2 on any node. Nothing is migrated; stop the removed node and wipe its data before it is ever re-added. Removing the leader is allowed: it steps down once the removal commits (one election, about 0.5–1.5 s). The change is refused with 503 when the remaining voters would not form a quorum the leader recently heard from, and with 400 for the last voter. A removed node that keeps running cannot disrupt the cluster: members refuse its pre-vote, so it never bumps the term.

Replicated: add a node

  1. Start it empty: --mode=replicated --node-id=r4 --addr=10.0.0.24:9000 --join=10.0.0.21:9000 --cluster-secret-file=… --consistency=<same>. Until it is added it is joining and refuses writes.
  2. The coordinator (the member with the lowest ID) writes the next membership version and pushes it to every member, which persists it in <--data>/cluster_membership.json. Each member catches r4 up with every write it originated, then live batches, and copies every key it holds to r4 (put-if-absent, so a copy never overwrites a newer write or resurrects a delete).
  3. Done when every member shows r4 SYNC (veltrix replication, resync_pending=false) and logged [transfer] migration done. Each member sends every key it holds.

Replicated: remove a node

veltrix cluster remove-node r2: r2 is drained first — it refuses new writes until every write it originated is acknowledged by every replica (default wait 2 min, --wait-ms); if that does not happen nothing is removed (409) and r2 serves writes again. Then the new membership is pushed, and r2 sends every key it holds to the remaining members, deleting each only after all of them acknowledged it. Move clients off r2 first; while another member is down nothing can be drained. A dead node cannot be drained: --force removes it without the drain, and writes it acknowledged but never replicated (possible with --consistency=eventual) are lost.

What wins on restart

Once any change was made online, the persisted membership (raft: configuration entries in the log and snapshot; replicated: cluster_membership.json) wins over --peers on every restart. A cluster that never changed membership online keeps following --peers. --join applies only to a node with no persisted membership that is not named in --peers, so one pod template can carry both flags.

Limits and recovery Every node must run the new build before the first change (older nodes do not know learners or pre-vote). Replicated mode has no consensus: a member that misses a change pulls it later, which leaves a window of disagreement during a partition. --force-new-cluster is dangerous and for disaster recovery only: in raft mode, after a quorum is permanently lost, stop every survivor and start one with it (it commits its whole log, including entries that were never committed), remove the flag, then wipe every other former member and add it back with --join — never force two nodes, or while the old quorum may still run. In replicated mode it moves cluster_membership.json aside and starts from --peers; do it on every node with the same --peers. The Kubernetes operator still scales by restarting pods with a new --peers, which stops working once a change was made online.

Rolling upgrade and rollback

One node at a time; a node is done when it has caught up, not when its process is up. Stop with SIGTERM (a clean stop runs the WAL checkpoint), swap the binary, start on the same --data.

Cluster-aware client behavior

The bundled Go client in the server repo (client.NewClient) is a real cluster client, not a dumb connection wrapper. The other SDKs live in the separate Veltrixdb-client repository; check their own docs for cluster routing.

Known limitations

Raft reads are local by default raft mode gives linearizable writes via quorum commit, but by default reads are served from whatever state has been locally applied on the node handling the request — a client that reads immediately after a write acknowledged by a different node may observe stale data. Start the cluster with --linearizable-reads when you need never-stale GET / MGET, and budget one heartbeat round-trip per read; on v1.13 and earlier other read commands stay local (Fixed in v1.14 every read is covered).
Replicated mode is not linearizable replicated mode is primary-copy replication built for durability across copies, not for ordering. It has no single-writer ordering, so two concurrent writers to the same key are not linearizable under any -consistency setting, including strong, and two clients writing one key through different nodes at the same time can leave the nodes holding different values for it permanently (no conflict resolution; open). Use raft mode when write linearizability matters, or write each key through one node. On v1.16 and earlier a replica that was down while its primary restarted also never receives the writes and deletes made before that restart (Fixed on main after v1.16 — ships in the next release see Replication & CDC).
Remaining gaps Namespace, hash-field, vector, text, secondary-index and list/set writes route through the coordinator in every mode (raft replays them as dedicated FSM ops; replicated mode ships their composite-key KV effects as ordinary replication traffic), and IDXCREATE / IDXDROP replicate in both modes via @idxdef/ keys. What remains node-local: RANGE / SCANCUR reads. Search indexes are rebuilt at every start and searches are refused until done. On v1.13 and earlier a node marked failed never returns to active (it stays recovering) — Fixed in v1.14 it returns after 2 good recovery pings. The cross-region shipper repl-ship has no back-pressure to the source and resolves conflicts last-write-wins only. See Data Model for what these operations do.

Decision guidance

If you need…Use mode
Maximum throughput, single node, no HA requirementstandalone
Linearizable writes with automatic failover across a quorumraft
Strongly consistent reads with no staleness windowraft with --linearizable-reads — every GET / MGET (every read since v1.14) is quorum-confirmed via the ReadIndex fence, at one heartbeat round-trip per read (leader-only; followers redirect).
Durability across multiple copies with the lowest write latency, and can tolerate eventual convergencereplicated with -consistency eventual
Durability across a majority of copies before acknowledging a write, without requiring all replicas upreplicated with -consistency quorum
Every replica confirmed before ACK, and can tolerate rejected writes when a replica is downreplicated with -consistency strong
Namespace / hash-field / vector / list / set / secondary-index writes to be cluster-wideraft or replicated — routed through the coordinator in both (index definitions replicate too); RANGE/SCANCUR reads stay local, while searches, QUERY and IDXQUERY fan out.