NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee what's new →

High availability

This page is for operators who run Dakera v0.12.3 on more than one node. It explains how a cluster replicates and elects a leader, the settings it needs, how to add, remove and upgrade nodes, and what to monitor.

A Dakera cluster uses gossip-based membership (SWIM over UDP), lease-based leader election and versioned replication: every node accepts writes and holds a full copy of the data. Nodes find each other through seed addresses; the elected leader runs the background jobs that rewrite stored data. Each node uses its own storage (on S3, its own bucket).

Architecture diagram of a three-node cluster. Your agent writes to any node; here Node 2 takes the write, keeps a full copy and syncs the change to Node 1 and Node 3. Node 1 is the elected leader and runs the background jobs; Node 3 is a follower. Nodes 1 and 3 gossip and reconcile. Each node has its own storage: its own disk or its own S3 bucket. Membership uses SWIM gossip and the leader is elected with a lease.Architecture diagram of a three-node cluster. Your agent writes to any node; here Node 2 takes the write, keeps a full copy and syncs the change to Node 1 and Node 3. Node 1 is the elected leader and runs the background jobs; Node 3 is a follower. Nodes 1 and 3 gossip and reconcile. Each node has its own storage: its own disk or its own S3 bucket. Membership uses SWIM gossip and the leader is elected with a lease.
Any node accepts writes and holds a full copy; the elected leader runs the background jobs.Full size ↗Full size ↗

Cluster environment variables

VariableDefaultDescription
DAKERA_CLUSTER_MODEfalseEnable multi-node cluster
DAKERA_CLUSTER_NODE_IDgenerated onceStable node identifier (e.g. node-1). If unset, generated once and kept in {data root}/cluster-node-id so the node stays the same member across restarts. DAKERA_NODE_ID is the legacy alias
DAKERA_CLUSTER_SECRET—v0.12. Shared secret, at least 16 characters, the same on every node. Every node-to-node request under /internal/* must carry it and gossip packets are authenticated with it. Required on every node, fresh install or upgraded, from v0.12.1: a node without it exits with code 78 before serving (on v0.12.0 an upgraded node started without it, unauthenticated)
DAKERA_ELECTION_LEASE_MS15000v0.12. Leader lease and the one failure-detection timing; every other cluster timing is derived from it (1000–600000)
DAKERA_CLUSTER_TOMBSTONE_RETENTION_SECS604800v0.12. How long a delete tombstone is kept; set it above the longest node outage you tolerate without wiping the node
DAKERA_CLUSTER_MAX_CLOCK_DRIFT_MS60000v0.12. A replicated change whose version is further ahead of this node's clock is refused and retried — keep clocks NTP-synchronized
DAKERA_CLUSTER_OUTBOX_DIR<data root>/cluster-outboxv0.12. Durable outbox for changes a peer has not acknowledged
DAKERA_CLUSTER_SEED_NODES—Comma-separated gossip bootstrap addresses, for example dakera-2:7946,dakera-3:7946. DAKERA_CLUSTER_SEEDS is the legacy alias
DAKERA_GOSSIP_PORT7946SWIM gossip port — must be open between nodes
DAKERA_CLUSTER_BIND_ADDR0.0.0.0:<gossip port>Gossip bind address (useful on multi-NIC hosts). DAKERA_GOSSIP_BIND is the legacy alias
DAKERA_API_ADVERTISEhttp://localhost:<port>The API address peers use to reach this node; its host is also the advertised gossip host when the bind address is 0.0.0.0. Set it on every node
DAKERA_REDIS_URL—Optional Redis shared by the nodes, for example redis://redis:6379: a shared cache, and one rate limit for the whole cluster instead of one per node

How the cluster works in v0.12

Any node accepts writes. There is no leader-only write path. A write is committed on the node that took it, then sent to every peer's /internal/sync/* route and awaited (5 s per peer). The client is answered 200 whatever the peers said. A peer that did not acknowledge gets the change later from the node's durable outbox. Every node holds a full copy of the data; the design favors availability (both sides of a network partition keep accepting writes and converge when it heals).

Versions and tombstones

Clock drift

A change whose version is more than DAKERA_CLUSTER_MAX_CLOCK_DRIFT_MS (default 60000) ahead of the receiver's clock is refused and retried. A node whose own clock is that far ahead of more than half its peers refuses local writes (503) and reports degraded with clock_ahead_ms in /health until the clocks agree. Keep NTP running on every node.

Reconciliation

A (re)starting node merges completely with a live peer (the leader first) before it answers digests or stands for election; it waits up to two leases for a peer. Every 5 minutes each follower merges with the elected leader in both directions, by version, over every namespace either side holds, including API keys, quotas, namespace configs, sidecars and attachments, and the leader merges with every live follower. Per-namespace digests are cached, so an unchanged cluster costs nothing to compare. Each node persists since when its data is complete (data_since): a node whose data starts later than a record's can never make others delete it.

Leader election

One timing governs failure detection, DAKERA_ELECTION_LEASE_MS (default 15000, accepted range 1000 to 600000; a value outside it is an invalid value). Every other timing derives from it:

Timing Derived value At the default
Leader renewal / heartbeat lease / 5 3000 ms
Election timeout lease x 2/3 10000 ms
Candidacy backoff lease / 15 1000 ms
Peer health check interval, and outbox retry lease / 3 5000 ms
Peer health timeout lease x 2/3, at least three gossip periods 10000 ms
Startup reconciliation wait 2 x lease 30 s

A longer lease suits a WAN or a loaded network; a shorter one detects failures faster. Reconciliation runs every 5 minutes regardless.

Durable outbox

Broadcasts a peer has not acknowledged are written to DAKERA_CLUSTER_OUTBOX_DIR (default {data root}/cluster-outbox) before they count as queued, and replayed in order at startup. The outbox holds at most 64 MiB; the oldest entries are evicted when it is full and are then repaired by reconciliation. A change a peer refuses for good (a 4xx other than 408, 429, 401 and 403) is dropped, logged and repaired by reconciliation instead of blocking that peer. A write whose client went away after it was committed is still replicated.

Authentication

Cluster mode requires DAKERA_CLUSTER_SECRET (at least 16 characters, identical on every node) on every node, fresh install or upgraded (v0.12.1), and a stable DAKERA_CLUSTER_NODE_ID on a fresh install. A node without the secret exits with code 78 before it serves anything; dakera --check-config reports a missing or short secret. On v0.12.0 an upgraded node could start without it, which left POST /internal/sync/key/add open: anyone who reached the API port could mint a SuperAdmin key. An upgraded node without a node id generates one, keeps it in {data root}/cluster-node-id, and is degraded in /health (cluster_auth) until it is set. /internal/* requires the secret and answers 404 on a node that is not in cluster mode; gossip packets carry an HMAC-SHA256 tag with a send time and nonce, and replays are dropped. The secret travels in every peer request and traffic is not encrypted: use a private network or TLS between nodes. See Security.

One S3 bucket per node

Cluster nodes must not share one S3 bucket as their store. Every S3 node keeps a heartbeat marker (_dakera_cluster/nodes/<node id>.json, refreshed every 30 s). A node that finds another node's live marker (younger than 120 s) in its bucket refuses to boot on a bucket that began its life on v0.12 (exit 78) and starts with a topology warning and shared_store degraded on a bucket that already held data. Backups are written to the backup store of the node that took them (node_id in GET /admin/backups); a restore or download through another node answers 409 naming the owner, unless the nodes share one backup bucket.

Observing a cluster

GET /admin/cluster/status (global admin) reports cluster_id, state (healthy, degraded, initializing, maintenance), node_count, total_vectors, namespace_count, redis_healthy, the node's election view (state, current_term, leader_id, fencing_token, lease_remaining_ms, member_count, alive_members, quorum) and, in cluster mode, replication:

replication field Meaning
outbox_entries Broadcasts queued for peers that have not acknowledged them
outbox_durable The outbox is on disk and survives a restart
sync_failures_total, outbox_dropped_total, refused_total Failed, evicted and permanently refused broadcasts since start
reconciled_namespaces_total, pushed_namespaces_total Namespaces where reconciliation applied or pushed newer records
versioning_active, tombstones Versioning on, and the delete tombstones held
data_since_ms, startup_reconciled, reconcile_source Completeness of this node's data and the peer it reconciles against
clock_ahead_ms Present while this node's clock is too far ahead (writes refused)

Compare the vector counts and replication across nodes to see whether the cluster has converged. Metrics: dakera_cluster_sync_outbox_entries, dakera_cluster_sync_failures_total, dakera_cluster_sync_outbox_dropped_total, dakera_cluster_sync_outbox_persist_failures_total, dakera_cluster_sync_refused_total, dakera_cluster_sync_superseded_total, dakera_cluster_sync_held_entries, dakera_cluster_replication_queue_dropped_total, dakera_cluster_reconcile_total, dakera_cluster_reconcile_namespaces_total, dakera_cluster_reconcile_pushed_namespaces_total, dakera_cluster_tombstones, dakera_cluster_tombstones_collected_total, dakera_cluster_clock_ahead_ms and dakera_cluster_clock_skew_refused_writes_total. /health degraded components that concern clusters: cluster_gossip, cluster_outbox, cluster_auth, cluster_sync_held and shared_store. The v0.12 alert rules cover held changes, dropped outbox entries and clock skew (Operations).

Maintenance mode

POST /admin/cluster/maintenance/enable with {"reason": "...", "node_ids": [...], "reject_requests": true, "duration_minutes": 30} puts the node that receives the request into maintenance mode. It is held in memory by that node alone (not replicated, ended by a restart); send the request to each node that should enter it. With reject_requests every request outside /admin, /v1/admin, /ops, /health, /metrics and /internal is answered 503 with Retry-After; gRPC calls other than Health fail UNAVAILABLE. POST /admin/cluster/maintenance/disable is 409 while maintenance jobs run unless force is set. GET /admin/cluster/maintenance shows the state.

Operating limits

Scaling procedures

Adding a node

# 1. Create the node's own S3 bucket, then start the node with seeds pointing at the cluster
docker run -d \
  --name dakera-4 \
  -e DAKERA_CLUSTER_MODE=true \
  -e DAKERA_CLUSTER_NODE_ID=node-4 \
  -e DAKERA_CLUSTER_SECRET=$DAKERA_CLUSTER_SECRET \
  -e DAKERA_CLUSTER_SEED_NODES=dakera-1:7946,dakera-2:7946 \
  -e DAKERA_API_ADVERTISE=http://dakera-4:3000 \
  -e DAKERA_S3_BUCKET=dakera-4 \
  -e DAKERA_ROOT_API_KEY=$DAKERA_ROOT_API_KEY \
  -p 3303:3000 -p 7949:7946/udp \
  ghcr.io/dakera-ai/dakera:0.12.3

# 2. Verify the node joined the cluster
curl -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  http://localhost:3000/admin/cluster/nodes

# 3. The new node merges with the leader before it answers digests or stands for election
curl -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  http://localhost:3000/admin/cluster/status  # replication.startup_reconciled: true

Removing a node

# Stop it gracefully (SIGTERM) so it announces a leave: a node that is lost for good
# keeps counting toward the quorum, and no admin endpoint removes a member.
docker stop dakera-4 && docker rm dakera-4

Rolling upgrade procedure

Upgrade cluster nodes one at a time, so the cluster keeps serving throughout. A cluster that already runs v0.12.0 with DAKERA_CLUSTER_SECRET set upgrades to v0.12.1 this way, keeping the same secret. A v0.12.0 cluster without the secret, or a v0.11.108 cluster, has extra rules:

Full procedure: Upgrading to v0.12.

StepCommandWhat happens
1. Enter maintenance (optional)POST /admin/cluster/maintenance/enable on that node with {"reason":"upgrade","reject_requests":true}The node answers 503 with Retry-After to everything outside /admin, /v1/admin, /ops, /health, /metrics and /internal; health probes keep answering, so a load balancer keeps routing to the node unless you also take it out of rotation. Held in memory by that node alone
2. Upgradedocker pull && docker restartStop the node, start the new image on the same data and environment
3. HealthGET /health/readyConfirm the new version is ready (/health lists config_warnings and degraded components)
4. ConvergeGET /admin/cluster/statusWait for replication.outbox_entries to drain and startup_reconciled: true
5. Repeat—Move to the next node (a restart ends maintenance mode; POST /admin/cluster/maintenance/disable ends it explicitly)

Docker Compose cluster

The dakera-deploy repository has a three-node Compose file (docker/docker-compose.ha.yml, with Traefik, Redis and MinIO). It was written for v0.11: its nodes share one MinIO bucket, set no DAKERA_CLUSTER_SECRET, and set variables v0.12 does not read. A v0.12.1 node refuses each of these (the shared bucket and unknown names on a fresh install; the missing secret on every node). Before you start it with v0.12.1:

# After adapting the file as above
git clone https://github.com/dakera-ai/dakera-deploy
cd dakera-deploy/docker
cp .env.ha.example .env.ha  # set your keys and secrets
docker compose -f docker-compose.ha.yml --env-file .env.ha up -d

# Traefik publishes the API on port 3100
curl -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  http://localhost:3100/admin/cluster/status

Kubernetes

The published Helm chart (dakera-helm) does not configure cluster mode: dakera.replicaCount scales one Deployment whose replicas share the same storage settings, and it sets no gossip seeds, cluster secret or per-node bucket. For a v0.12.1 cluster on Kubernetes, run one workload per node with its own DAKERA_CLUSTER_NODE_ID, bucket and stable address, the same DAKERA_CLUSTER_SECRET (from a Kubernetes Secret), the settings in the table above, and probes on /health/live and /health/ready.

Monitoring HA health

Key signals in a clustered deployment (all emitted by the server):

SignalAlert onMeaning
dakera_cluster_sync_outbox_entriesgrowing, or non-zero for longBroadcasts a peer has not acknowledged
dakera_cluster_sync_outbox_dropped_totalany increaseThe outbox was full and evicted entries (reconciliation repairs them)
dakera_cluster_sync_held_entries> 0 outside a rolling upgradeChanges held for peers that still run v0.11
dakera_cluster_clock_ahead_ms> 0This node's clock is too far ahead; it refuses writes
dakera_component_degraded{component}> 0cluster_gossip, cluster_outbox, cluster_auth, cluster_sync_held, shared_store
GET /admin/cluster/status election.alive_members< expected node countA node has left or failed; compare with election.quorum

Gossip ports

The defaults suit most deployments. Change them for a port conflict or a multi-NIC host:

VariableDefaultWhen to adjust
DAKERA_GOSSIP_PORT7946Port conflict with another service
DAKERA_CLUSTER_BIND_ADDR0.0.0.0:<gossip port>Multi-NIC hosts: bind to one interface
Network requirements: Gossip is UDP on the configured port; replication, voting and heartbeats are HTTP requests to the nodes' API ports (/internal/*). Allow both between cluster nodes. A longer DAKERA_ELECTION_LEASE_MS suits high-latency networks.

Disaster recovery

For cross-region disaster recovery:

Admin endpoints

EndpointDescription
GET /admin/cluster/statusCluster state, election view and replication state
GET /admin/cluster/nodesNodes with state, role, address, vector count and last heartbeat
GET /admin/cluster/replicationReplication information
GET /admin/cluster/shards, POST /admin/cluster/shards/rebalanceShard map and rebalancing
GET /admin/cluster/maintenanceMaintenance state of the receiving node
POST /admin/cluster/maintenance/enable, /disableMaintenance mode of the receiving node (disable is 409 while maintenance jobs run, unless force)

Production checklist

Related

Deployment → Troubleshooting → dakera-deploy repo ↗

Want zero-ops HA? Dakera Cloud handles clustering, replication, and failover — you ship agents, not infra.

Join Cloud Waitlist →
Stay sharp on agent memory
Benchmark results, SDK releases, and production patterns. Under 500 words per issue.
✓ You're in. First issue lands soon — watch for Dakera in your inbox.