For operators sizing a Fief cluster: partition count, lease timing, and transfer pacing. Assumes the guarantees and the full option reference; this page is the "how to choose", not the option list.
Three knobs matter more than the rest, and they fall into two very different classes. Partition count is a one-time, cluster-wide, irreversible decision. Lease timing and transfer pacing are per-node tuning you can revisit on any rolling restart — get them wrong and you pay in latency or churn, never in correctness.
Partition count: the one irreversible decision
partitions (default 1024) splits the keyspace into vnodes by an
immutable hash. It is immutable cluster-wide — written by the first node
ever to start the namespace, checked against every later joiner, fatal to
change without stopping every node — because changing it remaps every
key's home. There is no live repartitioning; it is a full stop, reconfigure,
restart, not a rolling change.
Pick generously up front rather than growing into it. More vnodes than
nodes is what lets the planner spread load evenly and rebalance in small
increments as membership changes — too few vnodes for your eventual node
count caps how finely ownership can ever be divided. 1024 is comfortable for
most clusters; 4096 is reasonable headroom if you expect to scale out to
many tens of nodes. There is no code-enforced ceiling, but more vnodes also
means more fief_table rows and more planner bookkeeping per pass, so
"as many as convenient" is not the answer either — undersizing the ring is
the regret operators report more often than oversizing it. See
configuration for the
mismatch-at-join mechanics.
Lease TTL and margin: the one conservative timing
lease_ttl (default 5_000 ms) is the failover floor:
unavailability after a node death ≈ lease_ttl + planner reaction + per-key rebuildNo hint, no faster failure signal, and no configuration elsewhere can tunnel
under the TTL term — a nodedown or a health check can only make the
planner reaction term smaller, because the arbiter can never treat a
node as dead before its lease has actually expired on its own clock (a
"dead" node might merely be partitioned and still serving on the far side).
Shorter TTLs recover faster but renew more often (renew_interval, default
lease_ttl ÷ 5) and tolerate less clock drift and Postgres latency before a
healthy node self-fences by mistake; 3–5 seconds is comfortable against a
healthy Postgres. Size it against your arbiter's actual latency, not a
round number — failure has the full
timing argument.
lease_margin (default :auto = renew_rtt_budget + 2 × clock_drift_bound)
is the self-fence window before nominal expiry, and it must stay strictly
under lease_ttl — validated and fatal at startup otherwise, since a margin
at or above the TTL would fence a node before its lease could ever be live.
The :auto default already bakes in the conservative case (drift can swing
between extremes across one measurement interval, so the bound is doubled,
model-checked against a real counterexample at 1×); only override it if you
have measured your own renewal RTT and clock drift and want a tighter
number.
None of this conservatism carries over to leadership. leadership_ttl
gates only the planner's fenced writes — a wrong or absent leader is safe,
merely idle — so it can run as hot as its mechanism allows without the
lease layer's clock-skew caution. The only place a fast leadership_ttl
and campaign_interval pay for themselves is the compound failure (a
member and the leader dying together), where a quick re-election trims how
long that member's keys wait for someone to reassign them.
Transfer pacing: what actually bounds drain time
Two different knobs bound two different things, and conflating them is the easiest way to under-provision a deploy:
max_concurrent_transfers(default8) bounds open transfers — rows withprev_ownerset — not CAS throughput. It exists so a large rebalance (a join, a scale-up) does not flood every node with concurrent pull traffic at once; a joiner "pulls only the over-capacity vnodes it is owed, capped by this budget" regardless of how unbalanced the cluster is.sweep_rate/sweep_interval(defaults100keys per1_000ms, inside thevnode_imploptions) is the actual drain-time knob. Once a transfer is open, its keys move at this rate in the background (hot keys pull on first touch regardless, but the bulk of a vnode's cold keys ride the sweep) — this is what bounds how long a leave, a leader-driven rebalance, or a shutdown drain actually takes wall-clock time to finish, not the transfer count cap.
A node holding many keys per vnode needs either a higher sweep_rate or a
longer window to drain within. That window is
leave_on_shutdown (default 20_000 ms): the
sentinel that runs a graceful leave on deploy waits for exactly this
drain to finish, and a timeout is not unsafe — it degrades to an ordinary
node failure and the un-handed-over keys just rebuild cold — but it does
turn a should-be-clean deploy into cold-start churn. If your typical node's
share of the keyspace cannot realistically drain inside the default 20 s at
your configured sweep_rate/sweep_interval, raise sweep_rate, or raise
leave_on_shutdown (kept comfortably under your orchestrator's
termination-grace-period — see the runbook
for the deploy-observable behavior and configuration
for the option itself). leave_drain_interval (default 100 ms) is just
the re-check cadence during that wait and rarely needs tuning on its own.
Two smaller knobs round this out: pull_retry_interval (default 1_000
ms) paces how often a stalled first-touch pull retries, and
extract_deadline (default 5_000 ms, per key module — see the key
lifecycle) bounds how long a
single stuck key is allowed to hold up its vnode's freeze before it is
killed and rebuilt fresh on the new owner, rather than stalling the whole
handover indefinitely.
Rebalance cadence
rebalance_interval (default 1_000 ms, ± rebalance_jitter, default
interval ÷ 10) is how often the planner recomputes the desired
distribution and issues moves. It matters far less than the two knobs
above: presence hints already trigger an immediate pass on most membership
or ownership changes, so the interval mainly bounds how quickly a
missed hint (a severed presence channel — netsplit row
6) is caught on poll alone. Lowering it
trades a small amount of steady-state planner/arbiter chatter for slightly
faster convergence in that fallback case; the default is a reasonable
middle ground for most clusters.
Cluster scale
Fief assumes a full distributed-Erlang mesh and is sized for a modest node count carrying a very large key count — tens of nodes, not hundreds. Beyond roughly 60–100 nodes, default distributed Erlang itself (not anything Fief-specific) becomes the binding constraint; clusters at that scale should already be evaluating partial-mesh or custom-transport strategies independent of Fief.
Verified by
test/guides/configuration_test.exs— every default named on this page (partitions,lease_ttl,renew_interval,max_concurrent_transfers,rebalance_interval/jitter,sweep_rate/sweep_interval,leave_drain_interval) asserted against the actual validator code.test/fief/node_test.exs—describe "margin arithmetic"(the:autoformula, and bothlease_margin ≥ lease_ttlandhandle_fence_deadline ≥ marginrefusing to start).test/fief/planner_test.exs—"a joiner pulls only the over-capacity vnodes, capped by the transfer budget"and"a :leaving member drains: all its vnodes move away, budget-paced"—max_concurrent_transfersin action on the pure planning function.test/fief/key/vnode_impl_test.exs—"background sweep migrates an untouched key"and"the extract deadline kills a stuck key; it converges via {:not_here} → escheat"—sweep_rate/sweep_intervalandextract_deadlineexercised end to end.test/fief/node/shutdown_drain_test.exs—describe "shape gating"(leave_on_shutdowndefault20_000ms) anddescribe "graceful teardown"(the timeout path this section's drain-time arithmetic feeds into).
Design notes: docs/design.md §9 (operational notes: partition count,
TTL tuning, transfer pacing, cluster scale).