Skip to main content

Node discovery and membership

By the end of this page you will know exactly how a node finds its peers, what has to be reachable for that to work, the difference between starting a brand-new cluster and joining a running one, and how to troubleshoot a node that will not join.

How a node finds its peers​

Three discovery providers can contribute candidate peer addresses, controlled by the discovery list in [cluster] (default: all three, ["static", "dns", "swim"]; an unrecognized name is rejected at startup rather than silently ignored):

  • static: the addresses in seeds, a plain list of host:port strings. This is the only provider you need for a fixed set of machines or containers, and it's what every path in Forming a cluster uses.
  • dns: re-resolves a single name, dns_name, every dns_refresh (default 5 seconds), and treats every address that name currently resolves to as a candidate peer. This is the Kubernetes shape: point dns_name at a headless Service (for example scramdb-headless.default.svc.cluster.local), and each backing pod's address becomes a candidate automatically as pods come and go. dns present with dns_name unset is valid and simply inert, not an error, so leaving dns in the default list costs nothing on a non-Kubernetes deployment.
  • swim: contributes peers this node's own gossip has already confirmed alive by some other means, rather than new addresses to dial.

seeds and dns_name only ever supply addresses worth trying. What actually makes a dialed peer count as "known to this cluster" is the cluster transport handshake succeeding against it, and it staying alive under the membership layer described next.

How membership is maintained, and how a failure is noticed​

Once a peer is known, this node keeps probing it, and you'll see the result as one log line per transition: swim: member up: <node>, swim: member suspect: <node>, swim: member down: <node>. A node that stops responding moves from alive to suspect to down within a few seconds under default timings; a node that comes back before it's fully reaped rejoins under the same identity rather than starting over.

This detection layer is deliberately separate from, and generally faster to notice a dead leader than, the per-shard-group leader election that actually keeps data available. See Failover for both timers together and exactly what a connected client experiences while they run; this page is only about how a node gets seen in the first place, not what happens to in-flight queries while that's being sorted out.

Membership itself, not just liveness, changes through a different mechanism: an already-running cluster's own auto-join control loop is what actually admits a newly-seen node as a member and later promotes it to a full voter, with no admin command involved. That mechanism, and what it means for adding or removing capacity, is Scaling's subject.

Bootstrapping the first node versus joining an existing cluster​

These are two different shapes, and which one you get is decided entirely by what a node's seeds list contains the very first time it starts (a restart with existing local data repeats neither path; it resumes from what it already durably knows).

An empty seeds list always self-forms a new one-node cluster, immediately, with no waiting. This is the cluster-of-one starting point described in Forming a cluster.

A non-empty seeds list, on a genuinely fresh set of nodes started together, bootstraps them as one founding group. Each node waits, bounded by group0_bootstrap_timeout (default 60 seconds), until it has connected to as many peers as it has entries in seeds; once it has, the whole set, itself plus everyone it connected to, becomes the cluster's founding membership together. This never falls back to a partial set on timeout: a node that can't reach enough of its configured seeds within the timeout fails loud and exits, naming how many it expected against how many actually connected, rather than silently forming a smaller cluster than you asked for. This is the shape every multi-node path in Forming a cluster uses: every node lists every other node as a seed, and all of them start together.

bootstrap_expect founds the same way, without naming any peer's address up front. Set it instead of seeds, and every node waits for that many peers, discovered through dns or swim rather than a hand-typed list, before founding together; the wait is bounded by the same group0_bootstrap_timeout and fails loud on the same terms as the paragraph above. This is what removes a hardcoded seed list's one real weakness: a fixed address list is only ever correct at the exact replica count it was written for. See Forming a cluster for the full mechanism, its validation rules, and a working example.

Adding a node to a cluster that is already up and running uses a third, later mechanism: point the new node's seeds at any live members of that cluster, start it, and the running cluster's own auto-join loop discovers and admits it automatically, with no bootstrap wait and no admin command. See Scaling for that procedure end to end.

The one real footgun: an empty seeds list never fails; it always just quietly starts a brand-new one-node cluster. If you meant to add a node to an existing cluster and forgot to set seeds, you will not see an error. You will get a second, isolated cluster running next to your first one, and from either side it looks like a perfectly healthy small cluster; the only symptom is that a node you expected to see never shows up in either one's membership. Always double-check seeds names real, reachable members of the cluster you actually intend to join before starting a node you don't want bootstrapping on its own.

What must match, and what may differ​

Every node in one cluster must agree on replication_factor, default_buckets, tso_batch, group0_bootstrap_timeout, txn_lock_ttl, commit_v2, and everything under [cluster.swim], [cluster.consensus], [cluster.rebalance], [cluster.txn], and [cluster.transport]. These govern cluster-wide behavior; an inconsistent value across nodes is a misconfiguration, not a supported per-node tuning knob.

[cluster.consensus] is the current name for what used to be [cluster.raft]; the section is deliberately algorithm-neutral now: election timing, heartbeat period, and batch caps are properties of leader-based replication in general, not of one specific protocol, so the name no longer ties the config to Raft specifically. The old [cluster.raft] spelling is accepted forever as an alias, so nothing written against it stops working.

If you're using bootstrap_expect for seedless founding, it belongs on this must-match list too: every founding node needs the identical value (the count of every other voter in the set), the same way every node's seeds list has to agree on the same founding set today. Getting it wrong doesn't corrupt anything (each side's genesis is fenced by an incarnation check that stops two independently-formed groups from ever merging), but it does mean you end up with two small clusters instead of the one you meant to start.

Each node's own identity is expected to differ: node_name, advertise_addr, and its own seeds entry (every node lists its peers, never itself, though listing yourself is harmless, since the handshake rejects it). cluster_listen only needs to differ if you're colocating more than one node on the same host. learner differs deliberately, only for a node you specifically want as a permanent read-replica rather than a voting member.

Firewall and network requirements​

Config keyDefault portDirectionPurpose
cluster_listen / advertise_addr7190Every node to every other node it needs to reach, both directionsThe single cluster transport port. Membership gossip, consensus traffic, and cross-node query and transaction traffic all multiplex over the same connection between any two peers; there is no separate port per subsystem.
pg_address5432Client -> any node you want it to queryOrdinary PostgreSQL wire protocol. No cluster-specific port; a client connects the same way it would to a single-node instance.
metrics_port9090Your monitoring -> each nodePrometheus /metrics and /health. Not required for the cluster itself to function; required to observe it.

What each node must actually be able to reach:

  • Every peer's advertise_addr, on its cluster_listen port. This is the address that's dialed, not cluster_listen itself. If a node publishes an address other nodes can't route to (a container's internal hostname with no DNS entry outside that container network, for example), peers will never see it come up, no matter how correct its own config is.
  • A DNS resolver capable of resolving dns_name, only if you're using the dns discovery provider (the Kubernetes shape above). Not needed for a static-seeds-only deployment.
  • Nothing beyond the cluster transport port for membership gossip specifically. It has no port of its own; it rides the same connection as everything else in the table above.

If you're terminating mTLS between nodes (tls_cert / tls_key / tls_ca, all three set together or all three omitted), that's still the same cluster_listen port; it doesn't add a second listener.

Troubleshooting​

A node never logs swim: member up for a peer it should see. Almost always a one-way or fully blocked reachability problem to that peer's advertise_addr, not its cluster_listen address. The two can differ (a container's internal bind address versus the address other nodes are told to dial), and only the latter matters to everyone else. Confirm you can open a TCP connection to the peer's advertised host:port from this node specifically, not just ping it.

A node forms its own isolated cluster instead of joining. See the footgun above: check that seeds is actually non-empty and actually points at reachable members of the cluster you meant to join. A cluster that has silently split this way looks, from either side, like a perfectly healthy small cluster.

DNS seeds versus static IP seeds. seeds (the static provider) accepts a DNS name in the host:port form too, not just a literal IP; the difference from the dedicated dns provider is timing. A static entry is resolved once, when it's first dialed. The dns provider's dns_name is re-resolved on an interval (dns_refresh) specifically so a name that now resolves to a different or additional set of addresses, a Kubernetes headless Service being the working example, is picked up automatically without a restart. Prefer dns for any name whose backing addresses you expect to change over the cluster's lifetime; static (or a literal IP) is enough for a fixed, stable peer.

Inspecting current membership. There is no SQL statement or CLI command that lists cluster membership today; a SHOW CLUSTER-shaped query gets a normal SQL parse error, not a membership report, regardless of what you might see referenced elsewhere. The working ways to check are the logs (grep for swim: member lines, one per transition, per node) and the metrics endpoint's connection-level counters (scramdb_cluster_peer_connects_total, scramdb_cluster_peer_disconnects_total, scramdb_cluster_reconnect_attempts_total), which tell you whether a node's connections are stable or actively churning, even though they don't hand you a live roster by themselves.

Next​

  • Forming a cluster to put this into practice from scratch.
  • Scaling for what happens once a new node is discovered: promotion to a full voter, and the honest limits of removing one.
  • Failover for the two timers that govern how fast a failure is noticed and what a connected client experiences while it's happening.