Skip to content

High Availability & Clustering

IndentiaDB clusters replicate through the Raft consensus algorithm: writes commit on a quorum, a failed leader is replaced by election, and lagging followers self-heal via snapshot install. This page covers the consistency guarantees clients can rely on and the operational knobs that keep replication robust under real-world load.

Upgrading an existing cluster

Two recent changes are deliberately not behaviour-neutral: the default Raft timing profile is slower (see Raft Timing Defaults), and the replicated command format is now v9, which activates one way per data directory (see Upgrades and Command-Format Activation). Read both sections before upgrading.


Read Consistency

Default reads: local, eventually consistent

By default every node answers /sparql queries from its local state. Reads are fast and remain available during partitions, but a follower (or a partitioned minority leader) can serve stale data.

Linearizable reads: consistency=strong

Clients that need read-your-writes / linearizable semantics opt in per request:

# Query parameter
curl "http://node:7001/sparql?consistency=strong&query=..."

# Or header (useful for POST bodies)
curl -X POST http://node:7001/sparql \
  -H "X-Indentia-Consistency: strong" \
  -H "Content-Type: application/sparql-query" \
  -d 'SELECT ...'

A strong read executes a Raft read-index barrier before the query: the node confirms it is still the quorum-acknowledged leader and waits until its state machine has applied everything committed up to the read point. This fences the classic failure mode where a stale, partitioned leader still believes it is leader and would serve outdated data.

Barrier outcome Response
Quorum-confirmed leader, caught up Query executes locally
Node is a follower with a known leader 307 Temporary Redirect with a Location header pointing at the leader
Leader unknown / quorum not confirmed 503 Service Unavailable — fail closed, never stale

The 307 Location is a cluster-internal address

The redirect points at the leader's cluster-internal address (e.g. a Kubernetes headless-service DNS name), which only resolves inside the cluster network. External clients should route strong reads through a load balancer that targets the leader, or run inside the cluster.

The barrier costs one quorum round-trip plus a catch-up wait; default reads are unaffected.


Write Semantics

Writes are accepted on every node but are leader-authoritative: a follower does not proxy your HTTP request — it forwards the write intent (the original SPARQL UPDATE, Graph Store operation, GraphQL mutation, or Elasticsearch mutation) over the authenticated cluster transport. The leader re-authorizes the actor, evaluates the update against leader-owned state, replicates the result through Raft, and the follower returns the committed outcome to the client. No client-side redirect handling is needed for /update, GraphQL, or the ES API.

Key guarantees:

  • Idempotent retries — each logical write carries a stable mutation identity bound to a semantic digest of its content. Retrying the same operation (e.g. after a timeout) returns the original committed result instead of applying twice; the same identity with different content is rejected as a conflict. The at-most-once window is five minutes — retry an ambiguous write with the same identity within it.
  • Fail closed — missing leadership, an incompatible rolling-upgrade state, or an unavailable shard replica yields a bounded, retryable error. A cluster node never falls back to applying a write locally outside Raft.
  • Bounded batches — Elasticsearch bulk requests are split into bounded chunks with order preserved and per-chunk results.
  • Draining nodes refuse writes cleanly — a node that is shutting down answers new writes with 503 + Retry-After: 1 so clients retry elsewhere or after failover (see Graceful Shutdown and Leader Handoff).

Raft Timing Defaults

The default Raft timing profile is 1000 / 3000 / 5000 ms (heartbeat interval / minimum election timeout / maximum election timeout), raised from 150 / 300 / 500 ms. The new defaults apply to the server's [cluster] configuration, the sample configuration produced by indentiagraph generate-config, and the Kubernetes operator CRD.

[cluster] key Previous default Current default
heartbeat_interval_ms 150 1000
election_timeout_min_ms 300 3000
election_timeout_max_ms 500 5000

Why: in the Raft library IndentiaDB uses, the heartbeat interval doubles as the deadline for every AppendEntries RPC, and a timed-out RPC is retried immediately. With a 150 ms heartbeat, a multi-megabyte log entry that took longer than one heartbeat to transfer was re-sent forever. A 1 s heartbeat gives every AppendEntries a 1 s budget, and 3–5 s election timeouts stop elections from firing while a follower is merely busy applying a large entry.

Breaking change: slower nominal failover

Worst-case leader failover is now roughly 3–6 seconds. On an idle cluster this is slower than before; under large-write load it is much faster than the old profile, which could take up to about a minute because of election churn and re-streamed entries. Make sure client retry policies tolerate a few seconds without a leader.

  • Explicit settings still win. A [cluster] block that sets the timing keys overrides the new defaults. Configurations that still pin 150/300/500 keep the old, livelock-prone profile and should be raised to 1000/3000/5000.
  • The server logs a startup warning when heartbeat_interval_ms is below 1000.
  • heartbeat_interval_ms must be lower than election_timeout_min_ms; the configuration is rejected otherwise.
  • No wire, snapshot, or on-disk format changes are involved: nodes with different timing profiles interoperate during a rolling upgrade.

Replication Robustness Under Large Writes

A single SPARQL UPDATE is one Raft log entry that cannot be split. A bulk write (DROP GRAPH, INSERT DATA with 100k+ triples) therefore produces a multi-megabyte entry, which historically could livelock replication: the per-RPC deadline was a single heartbeat interval, so the transfer timed out and was re-streamed forever. This is fixed by the timing profile above and by a detached, resumable send — no configuration change is needed to benefit:

Variable Default Effect
IG_RAFT_APPEND_TTL_FLOOR_MS 15000 Budget for the detached send of an entry-carrying AppendEntries RPC. The transfer runs in its own task that survives the per-heartbeat timeout; an identical retry within the budget joins the in-flight transfer instead of re-streaming the payload. When the budget expires the transfer fails and its pooled connection is evicted. Empty heartbeats bypass this path, so failure detection is unaffected.
IG_RAFT_MAX_PAYLOAD_ENTRIES 256 Maximum log entries per AppendEntries batch (range 1–1024). Batches are additionally cut at the 64 MiB transport ceiling, with the sent prefix acknowledged, so large catch-up batches can no longer overflow the gRPC message cap. Lower it (e.g. 16) for workloads with many multi-megabyte entries.
IG_RAFT_WAIT_LEADER_SECS 180 Boot-time ceiling for a rejoining node to observe a leader. Returns the instant a leader appears; the previous fixed 30 s crash-looped nodes whenever an election ran long.

While a large transfer is in flight, expect one Raft RPCError log line per heartbeat interval per target. That is the outer timeout firing on the caller, not a re-send — the detached transfer keeps streaming.

A follower that lags more than max_replication_lag entries (default 1000), or whose pending log tail exceeds IG_RAFT_SNAPSHOT_REPLAY_BYTES (see Snapshots and Restart Time), is caught up with a chunked snapshot install (3 MiB chunks) instead of individual log entries — the self-heal path that sidesteps entry size entirely.

Batch bulk writes

Even with these safeguards, issue bulk deletes/inserts as batches of ≤ ~2,000 triples per UPDATE rather than one giant operation. Oversized delta writes are also rejected by the delta overlay limit with HTTP 413.


Snapshots and Restart Time

The snapshot cadence bounds the log tail a restarting node must re-apply before it can serve, so it directly controls restart time. It is configured independently of max_replication_lag: previously the cadence was fixed at max_replication_lag / 2, so raising the lag threshold for replication reasons silently stretched restart replay.

Variable / [cluster] key Default Effect
IG_RAFT_SNAPSHOT_ENTRIES / snapshot_entries max_replication_lag / 2 Build a snapshot every N log entries. The environment variable wins over the config key. Applies to the metadata group and, unless overridden below, to every shard group on the node.
IG_RAFT_SHARD_SNAPSHOT_ENTRIES / shard_snapshot_entries Inherits the metadata cadence Separate cadence for data-shard Raft groups.
IG_RAFT_SNAPSHOT_REPLAY_BYTES 67108864 (64 MiB) The leader also sends a snapshot instead of log entries when a follower's pending retained log exceeds this many bytes, in addition to the max_replication_lag entry trigger. 0 disables the byte trigger.
IG_RAFT_SNAPSHOT_MAX_BYTES 128 MiB Upper bound for one fully reassembled snapshot. Clamped to one sixteenth of the container memory limit and to an absolute 8 GiB.
IG_RAFT_SNAPSHOT_MAX_VOCAB_EXPANDED_BYTES 64 MiB Separate budget for the vocabulary slot table a snapshot expands into during restore. Capped at 512 MiB and at half of the snapshot payload budget.

Snapshots can also be triggered by hand:

Endpoint Auth Effect
POST /cluster/snapshots/force Admin Builds a snapshot on the group leader. Body {"target": "meta"} for the metadata group, or {"target": "shard", "index": "<index>", "shard": <n>} for one shard. The snapshot is built and the covered log prefix purged; returns 202 {"status": "snapshot_triggered"}. A node that does not host or lead the group answers 503.
POST /cluster/shard/{group_no}/snapshot Admin Enqueues a snapshot build on one shard group (by its global group number from GET /cluster/state). Returns 202 Accepted; the build completes asynchronously. A member that does not lead the group answers 307 to the leader; an unknown group is 404. Under IG_PRODUCTION only the cluster-internal reconciler identity may call it.

Restart work itself is also cheaper: a persisted snapshot is decoded once per boot and handed out without copying, and the delta overlay is restored in bulk from the snapshot's sorted data.


Joining and Rejoining Nodes

join_timeout_ms ([cluster], default 30000) is the joiner's overall join budget, not a per-RPC deadline. A join proceeds in bounded rounds:

  • Each join RPC declares its own wait (join_wait_ms). The leader bounds each catch-up round to at most min(join_timeout_ms / 2, 10 s, declared wait − 1 s) and answers a non-fatal join_pending instead of being cancelled mid-wait.
  • The joiner's per-RPC deadline defaults to the leader's longest round plus 2 s (about 12 s), so a wedged or wrong seed costs at most ~12 s before the next seed is tried. Override it with IG_CLUSTER_JOIN_RPC_TIMEOUT_MS.
  • The budget resets whenever the leader reports replication progress for the new learner; only a join that makes no progress for join_timeout_ms fails.
  • Admission is idempotent: a member that is already tracked at the same address never gets a second learner entry (which would restart an in-flight snapshot transfer), and a learner is not promoted to voter before it has caught up.
  • Mixed versions interoperate: an older joiner receives a short leader round inside its old client deadline, and a newer joiner against an older leader falls back to the previous retry loop.

Give joins room

A (re)joining node may have to sit out long elections and install a full snapshot. A generous join_timeout_ms (for example 300000) keeps such a join from failing while it is still making progress.


Cluster Boot Checks

Cluster startup is fail-loud: misconfigurations that used to degrade silently now refuse to boot.

Condition Behaviour Opt-out
No working shard reconciler (for example, IG_CLUSTER_INTERNAL_BEARER_TOKEN missing) Refuses to boot IG_ALLOW_DISABLED_RECONCILER=true — development only, rejected under IG_PRODUCTION. While active, /ready reports reconcile_disabled when shard readiness conditions are enabled.
No durable shard data root can be derived from the Raft data directory Refuses to host shard groups on in-memory storage IG_SHARD_ALLOW_EPHEMERAL=true — development/test only; hosted shard data does not survive a restart.
Record-bearing legacy per-shard WALs (shard-N.wal) in IG_SHARD_DATA_DIR Refuses to boot; export them with indentiagraph shard-wal export and re-ingest IG_SHARD_WAL_RETIRE=ignore — the files stay untouched but their writes are not served. Refused under IG_PRODUCTION unless acknowledged with IG_PRODUCTION_RECOVERY_ACK=IG_SHARD_WAL_RETIRE.

The standalone per-shard WAL data lane is retired. On a standalone node, POST /cluster/tenant/{tenant}/update returns 503 shard_not_allocated: tenant data writes require a clustered deployment with an allocated shard.


Upgrades and Command-Format Activation

Replicated Raft commands use a versioned format, and every member must be able to decode a command before the leader proposes it. The current ClusterCommand format is v9:

Format Adds
v7 A durable, replicated command-format activation marker
v8 The BaseIndex lifecycle command for HA base-index attach. At activation, the replicated delta overlay switches to strict RDF set semantics on every replica.
v9 PublicationUpdate — guarded, atomic conditional SPARQL publications

How activation behaves:

  • Only after every member advertises support. The leader proposes a command that needs a newer format only when every voter and learner in the group advertises it. During a mixed-version rolling upgrade the new format stays inactive until the last member is upgraded; commands that require it fail closed in the meantime — a safe write refusal, never a decoding guess.
  • One way per data directory. Activation is recorded as a replicated marker that survives snapshot compaction. Once a command that requires the new format has committed, binaries that predate it can no longer run on that data directory.
  • Guarded publications (v9) must stay disabled on clients until every voter and learner advertises v9 and the v9 marker has committed.

Upgrade blue/green; never downgrade in place

Treat a format bump as a one-way door. Take and verify a backup — for HA clusters, a Backup Set — before upgrading, prefer upgrading into a new cluster (blue/green), and keep the previous cluster available until the new one is accepted. Rolling back means restoring the pre-upgrade backup, never starting older binaries on newer state.


Shard placement, splitting and integrity (v10 / metadata v6)

The distribution update retains one metadata Raft group and independent data shard groups. Routing uses the existing over-provisioned routing_num_shards; it does not use a hash ring or AP conflict resolution.

  • Set cluster.node_zone or IG_NODE_ZONE to the physical failure domain. Once any zone is set, unlabelled nodes and duplicate zones are excluded. Insufficient zones leave copies unassigned. Drain before changing a hosted node's zone. This does not alter Kubernetes scheduling.
  • Allocation weights estimated shard bytes and recent request rate. With auto_rebalance=true, it rechecks every 30 seconds and starts at most one improving relocation, using the existing catch-up and voter-removal protocol.
  • Periodic digest pages compare native RDF and vocabulary at the same applied log position. Lag is inconclusive; confirmed disagreement makes local cluster health red. This detects divergence but does not automatically repair it or certify every derived search index.

POST /cluster/indices/{index}/split with {"num_shards": 4} starts a durable split; GET reports progress, and {"abort": true} requests abort before routing cutover. Admin is required; production also requires the configured internal reconciler identity. The new count must divide the existing routing space and be a larger multiple of the old count (factor at most 16, at most 4096 shards). Only one split runs at a time.

Hidden child groups copy bounded pages and capture concurrent changes while the source remains readable and writable. After catch-up, a short write fence allows final RDF and retry/conditional-write/blank-node state to be verified on every assigned replica. Metadata switches routing atomically; children activate and old groups remain fenced. Missing replicas prevent commit. A 30-second cutover deadline chooses abort if verification cannot finish; releasing fences still requires quorum. Client retries can be needed during cutover, activation or quorum loss.

The first implementation supports native RDF shards with canonical tenant graphs. External base artifacts and non-transferable SQL/standalone ES state refuse the split. A single tenant routing key remains on one child; splitting an index does not split one indivisible hot tenant. Keep a verified backup and perform a separate controlled v10/v6 format migration before using this feature.

Graceful Shutdown and Leader Handoff

On SIGTERM a node shuts down in a fixed order:

  1. Write drain (always on). New writes are refused with 503 + Retry-After: 1, /ready fails with draining so load balancers stop routing new work, reads keep serving, and in-flight updates get a short, bounded wait to finish.
  2. Leader handoff (opt-in). With IG_DRAIN_HANDOFF=true, for the metadata group and every shard group it leads, the node waits until a successor and a quorum are fully caught up, stops sending heartbeats, waits out its leader lease, asks the best caught-up voter to start an election, and confirms that another node now leads. A rolling restart of a leader then avoids a full election-timeout gap. Every failure degrades to an incomplete handoff and never blocks termination.
  3. Ordered storage shutdown. HTTP stops, the Raft groups shut down, the Raft log storage is closed with a final durable sync, and only then do the Raft/admin and distributed-query listeners stop.
Variable Default Effect
IG_DRAIN_HANDOFF false Enables the leader handoff. Requires IG_CLUSTER_ID because the election request is identity-fenced; without it the handoff is skipped with a warning and only the write drain runs.
IG_DRAIN_TIMEOUT_MS 10000 Overall deadline for the drain: in-flight updates plus every group handoff.
IG_DRAIN_LEASE_MARGIN_MS heartbeat interval + 50 ms (minimum 100 ms) Extra wait past the leader lease before a successor is nudged. Values below the floor are raised to it.
IG_SHUTDOWN_TIMEOUT_SECS 20 Bounded wait for remaining background tasks after the ordered shutdown, so a stuck task cannot turn SIGTERM into a SIGKILL.

Size the termination grace period

Keep Kubernetes terminationGracePeriodSeconds comfortably above IG_DRAIN_TIMEOUT_MS plus shutdown work (a standalone node also writes a checkpoint on shutdown). The grace period remains the backstop for every step.


Torn-Log Recovery

An ungraceful shutdown or power loss can tear the tail of the Raft log. The strict default refuses to start on a corrupt entry — correct, but it can crash-loop a whole cluster when every replica tore its own tail. Several layers now prevent and repair this:

  • Durable acknowledgements. With IG_RAFT_LOG_SYNC=full, the log is flushed and fdatasynced before a write is acknowledged, so acknowledged entries survive power loss.
  • Torn-write detection. With IG_RAFT_LOG_FRAMING=true, each log value is written in a length- and checksum-carrying envelope, so a torn write is detected deterministically instead of being inferred from a decode failure.
  • Ordered shutdown. The log storage is closed with a final sync before the process exits (see above).
  • Boot-time recovery. Before its format checks run, a starting node classifies damage at the log tail. With IG_RAFT_LOG_RECOVERY=truncate it drops only entries strictly above the highest of its durable committed watermark, last applied entry, and last purged entry, then rejoins; the quorum re-replicates the missing suffix. Version skew, tampering, policy violations, and anything at or below that floor stay fail-closed. Storage-engine page corruption is never truncated: quarantine that replica's Raft data directory and let it re-seed through a snapshot install.
Variable Default Effect
IG_RAFT_LOG_SYNC full when IG_PRODUCTION is set, otherwise range full = flush plus fdatasync before acknowledging (survives power loss; costs one sync per acknowledgement). range = asynchronous flush without a device flush. range is refused under IG_PRODUCTION.
IG_RAFT_LOG_FRAMING false Write new log values in the checksummed envelope. Reading is unconditional: framed and unframed values interoperate in both directions. Captured once when the log opens; node-local.
IG_RAFT_LOG_RECOVERY unset (strict) truncate enables the bounded tail truncation described above, now also at boot. Permitted under IG_PRODUCTION with an audited warning — unset it after recovery.

Log framing and rollback

Releases that predate log framing cannot read framed values. Enable IG_RAFT_LOG_FRAMING only after every node runs a framing-aware release, and do not roll back below that release once framed entries exist.

Truncation is a break-glass option

If the recovering node is the sole survivor holding an entry that had committed on a since-dead quorum, that entry is lost — no local safeguard can prevent it. The flag is opt-in and every truncation is logged at ERROR level for auditability. Keep the strict default in normal operation.


Write Pipelining

By default every SPARQL UPDATE passes through one exclusive write gate. With IG_UPDATE_PIPELINE enabled, eligible updates take the gate shared, so parsing, staging, and proposing update N+1 overlaps with the Raft apply of update N. The feature is off by default; with it off, behaviour is unchanged.

Variable Default Effect
IG_UPDATE_PIPELINE off 1 or true enables pipelining for eligible updates.
IG_UPDATE_PIPELINE_DEPTH 16 Maximum number of concurrent shared writers (clamped to 1–256).
  • Eligible: ground-only INSERT DATA / DELETE DATA in cluster mode. Everything else — standalone mode, temporal writes, conditional Elasticsearch writes, SHACL-validated writes, dry runs, vector-index DDL, cascading deletes, and any update with a WHERE clause, CLEAR/DROP, or blank nodes — transparently takes the exclusive path.
  • Observability: shared admissions are counted in ig_update_pipeline_shared_total, runtime fallbacks in ig_update_pipeline_fallback_total.
  • Isolation: each eligible update is an independent Raft entry of ground quads, so every interleaving is a valid serialization. Requests sent sequentially on one client connection keep their order, and read-after-write is unchanged. The delta overlay limit stays hard: concurrent writers reserve capacity before they stage.

Constraints

  • Not together with vocabulary eviction. While IG_VOCAB_EVICTION is active (see Online overlay garbage collection), every update falls back to the exclusive path. Updates also fall back while a base-index cycle is journaling.
  • Whole cluster first. Enable pipelining only once every replica runs a release that includes it.
  • A retry of a mutation ID that is still in flight is recognised as a duplicate only on its second attempt; the window grows with IG_UPDATE_PIPELINE_DEPTH.

Health Probes

Endpoint Auth Purpose
GET /live public Process liveness. Always 200 while the process accepts connections — never restart-loops on recoverable faults. Use as the Kubernetes livenessProbe / startupProbe. Never point liveness at /ready or /health: a recoverable quorum or projection outage would become a restart loop.
GET /ready public Traffic readiness. 200 {"status":"ready"}, or 503 {"status":"not_ready","reasons":[...]} with flat condition codes. Use as the readinessProbe.
GET /health public Minimal status probe: {"status": "healthy"} and nothing else.
GET /health/details Admin Full HA evidence: role, term, cluster members, replication lag, cluster_enabled, ha_ready + failure reason, voter count and failure margin, sharding layout, and the readiness codes under readiness_reasons.

The ha_ready field in /health/details is a leader-local proof: it is true only when the node itself holds quorum-confirmed leadership evidence — followers do not echo their leader's claim, and standalone nodes always report false. See the API Reference for full response shapes.

Readiness conditions

A /ready 503 names condition classes only — never component identities or addresses — for example {"status":"not_ready","reasons":["meta_no_current_leader","shard_apply_lagging"]}. Diagnose further with authenticated /health/details and the logs.

Codes that mirror the traffic-admission gate include index_loading, draining, audit_sink_unhealthy, acl_index_uncertified, es_runtime_failed, es_projection_uncertified, linearizable_barrier_failed, and the meta_* family (for example meta_no_current_leader, meta_quorum_unavailable, meta_local_node_catching_up).

The probe is stricter than traffic admission: with shard conditions enabled it also fails while the shard substrate is degraded, so load balancers stop routing new work to a node that cannot reconcile shards, is installing a snapshot, or hosts leaderless or lagging shard groups. These codes only affect the probe, never requests that have already arrived:

Code Meaning Tunable
reconcile_disabled Cluster mode without a running shard reconcile loop (only reachable with IG_ALLOW_DISABLED_RECONCILER=true) —
reconcile_heartbeat_stale The reconcile loop made no progress within the window IG_READY_RECONCILE_STALE_SECS
snapshot_install_in_progress A Raft snapshot install is in flight, or saw activity within the hold-off without finishing IG_READY_SNAPSHOT_HOLDOFF_SECS
shard_no_leader A locally hosted shard group observes no leader —
shard_apply_lagging A local shard group's apply backlog exceeds the threshold (local backlog only) IG_READY_SHARD_MAX_APPLY_LAG
Variable Default Effect
IG_READY_SHARD_CONDITIONS the value of IG_PRODUCTION Enables the probe-only shard conditions. A malformed value fails the boot.
IG_READY_RECONCILE_STALE_SECS 300 Staleness window for reconcile_heartbeat_stale (well above the time membership sync may legitimately stall on one dead peer).
IG_READY_SNAPSHOT_HOLDOFF_SECS 15 Hold-off after snapshot-install activity; covers gaps between chunks.
IG_READY_SHARD_MAX_APPLY_LAG 1024 Apply-backlog threshold (log entries) for shard_apply_lagging.

The gauges ig_reconcile_heartbeat_age_seconds and ig_snapshot_install_inflight on /metrics expose the same signals.

Move liveness probes to /live first

Before enabling IG_READY_SHARD_CONDITIONS on an existing deployment, make sure no liveness probe points at /ready or /health; otherwise the stricter probe turns substrate degradation into restart loops.

Size startup and readiness windows from your data

A rejoining node may have to install a full snapshot before it is ready. The install budget is roughly 60 s + 1 s per MiB of snapshot, so size startup and readiness failure thresholds from your real dataset instead of a fixed short window.


Metrics and Alerts

Every node exports its own view on /metrics. Cluster families are only present in cluster mode — on a standalone node they are absent, not zero. Per-group families report the metadata group as shard="meta", group="0" and each hosted data-shard group as shard="<index>/<n>" with its global group number, so per-group expressions should aggregate with max by (shard, group) to de-duplicate across nodes.

Metric Labels Meaning
indentiagraph_indentiagraph_updates_total, indentiagraph_indentiagraph_updates_success_total, indentiagraph_indentiagraph_updates_failed_total — SPARQL updates executed, succeeded, failed (the write-side counterpart of the query counters)
indentiagraph_indentiagraph_update_duration_seconds histogram SPARQL update latency
ig_raft_follower_lag shard, group, follower Log entries a follower lags behind the leader
ig_raft_last_applied_index, ig_raft_snapshot_last_index shard, group Last applied index; last index covered by the most recent snapshot
ig_raft_persisted_snapshot_mtime_seconds, ig_raft_persisted_snapshot_size_bytes shard, group Age and size of the on-disk snapshot
ig_raft_apply_duration_seconds group_kind (meta / shard) State-machine apply latency
ig_shard_is_leader, ig_shard_term, ig_shard_voters, ig_shard_last_log_index shard, group Leadership, term, voter count, and log tip per group
ig_shard_membership_sync_failures_total, ig_shard_membership_sync_stuck — / shard, group Failed membership reconciliations; 1 while a group has failed 10+ times in a row
ig_cluster_shards_health color Shards per health color (green / yellow / red)
ig_cluster_dead_nodes — Nodes the metadata leader considers dead (0 when detection is disabled)
ig_reconcile_last_tick_age_seconds, ig_reconcile_ticks_total, ig_reconcile_rebalance_fail_streak loop Reconcile-loop liveness and rebalance failures
ig_command_format_activation_backlog — Command-format activations awaiting quorum
ig_dqs_active_subplans, ig_dqs_active_shuffles, ig_dqs_subplans_total, ig_dqs_subplans_rejected_total, ig_dqs_subplan_errors_total reason on rejections Distributed-query service load, rejections, and errors

The following Prometheus rules cover replication, cluster health, and distributed queries. Thresholds are starting points.

Alert Condition For Severity
IGRaftFollowerLagHigh max by (shard, group) (ig_raft_follower_lag) > 1000 5m warning
IGShardNoLeader no node leads a group that has voters 2m critical
IGShardMembershipSyncFailing rate(ig_shard_membership_sync_failures_total[10m]) > 0 15m warning
IGElectionChurn more than 3 term increases in 15 minutes — warning
IGRaftSnapshotBacklog more than 50,000 entries beyond the last snapshot 30m warning
IGRaftPersistedSnapshotStale on-disk snapshot older than 24 h while the log grows 1h warning
IGRaftApplySlow p99 apply latency above 1 s 10m warning
IGClusterShardsRed any red shard 2m critical
IGClusterShardsYellow any yellow shard (under-replicated; normal briefly after allocation) 30m warning
IGReconcileLoopStalled max by (loop) (ig_reconcile_last_tick_age_seconds) > 60 2m critical
IGRebalanceStuck more than 30 consecutive failed rebalance passes 10m warning
IGCommandFormatActivationStuck an activation pending 30m warning
IGDqsSaturated 16 or more concurrent subplans on a node 5m warning
IGDqsRejectingSubplans rejections for capacity/topology reasons (overload, not_hosted, no_worker) 10m warning
IGDqsSubplanErrors more than 0.1 failed subplans per second 10m warning
groups:
  - name: indentiagraph-raft
    rules:
      - alert: IGShardNoLeader
        expr: >
          sum by (shard, group) (ig_shard_is_leader) == 0
          and max by (shard, group) (ig_shard_voters) > 0
        for: 2m
        labels:
          severity: critical
      - alert: IGRaftSnapshotBacklog
        expr: max by (shard, group) (ig_shard_last_log_index - ig_raft_snapshot_last_index) > 50000
        for: 30m
        labels:
          severity: warning

The Kubernetes operator additionally generates its own release-gate alerts for the production profile — see Operator: Monitoring.


Production Checklist

  • Odd number of nodes (3, 5, 7) — see Deployment Overview.
  • security.mtls_enabled = true for the cluster transport; cluster.allow_plaintext is rejected under IG_PRODUCTION.
  • Keep the default timing profile (1000/3000/5000 ms) or higher, and a generous join_timeout_ms.
  • Probes: /live for liveness/startup, /ready for readiness; size startup windows from your snapshot size.
  • IG_RAFT_LOG_SYNC=full (the default under IG_PRODUCTION).
  • Set IG_CLUSTER_ID and consider IG_DRAIN_HANDOFF=true for rolling restarts without election gaps.
  • Upgrades across a command-format bump: backup first, blue/green, no in-place downgrade.
  • Monitor ha_ready (via authenticated /health/details), indentiagraph_indentiagraph_replication_lag and the per-group metrics above (via /metrics), and deploy the recommended alert rules.
  • Encrypt Raft state at rest and record IG_CLUSTER_ID with your recovery secrets.