Note: Company names, engineers, incidents, numbers, and scaling scenarios in this article are hypothetical — even when they resemble real ones. See the full disclaimer.

Wall: scaling membership needs gossip

It is 14:02 on a Friday at PaySetu. Karan, the platform-team lead, is staring at the network graph for the merchant-payments cluster. Each of the 480 service nodes is heartbeating every other node every 1 s — the simple all-to-all scheme that worked beautifully when the cluster had 30 nodes. The graph reports 4.6 Gb/s of pure liveness traffic and rising. Each node spends 38% of its CPU just sending and processing heartbeats. The membership view at any node is roughly 800 ms behind reality — a node that died at 14:01:50 will not be marked DOWN until 14:01:51 at the earliest, and probably 14:01:53 once the timeout expires. At 600 nodes, projections say the cluster will spend 60% of CPU on heartbeats alone. The all-pairs heartbeat scheme is not slow; it is mathematically incapable of scaling further. This chapter is the wall that closes Part 10 — the wall where every failure detector you have read about so far runs out of room, and where gossip becomes the only door forward.

All-to-all heartbeats are O(N²) in messages and O(N) in per-node bandwidth — fine at 30 nodes, dead at 600. The fix is not "tune timeouts" or "buy more bandwidth"; it is structural. Gossip-based membership spreads liveness information through randomised peer-to-peer exchanges with O(N · log N) total messages and O(log N) per-node bandwidth. SWIM, anti-entropy, and the protocols of Part 11 are all built on this insight. Part 10 ends here because every failure detector you have learned — heartbeats, phi accrual, fencing — assumed a small enough cluster that "every node knows about every node" was free. At scale, that assumption breaks, and you are forced into a different topology.

The arithmetic that closes the door

The simplest membership protocol is direct: every node sends a heartbeat to every other node every T seconds; if you have not heard from a node in k · T seconds, you mark it suspect. With N nodes and heartbeat interval T = 1 s, the total per-second message rate is N · (N − 1). Each node sends and receives 2 · (N − 1) messages per second. At N = 50, that is 2,450 messages per second cluster-wide and 98 per node — trivial. At N = 500, it is 249,500 messages per second cluster-wide and 998 per node — still livable but the per-node CPU starts to bite. At N = 5,000, it is 24,995,000 messages per second cluster-wide and 9,998 per node — and the cluster's network saturates before the CPU does.

The bandwidth picture is worse than the message-count picture. A heartbeat carries not just "I am alive" but the sender's view of the membership table — every node it knows about, with each node's last-seen timestamp and incarnation number. At N = 500, the membership table is ~30 KB per heartbeat (60 bytes per entry × 500 entries). Multiply by 998 heartbeats per node per second and each node is sourcing 30 MB/s of liveness traffic. With a 10 Gbit/s NIC that is 2.4% of capacity — but you also have to receive 30 MB/s, and process every entry to update your view. The processing cost dominates. Why N² traffic kills the cluster long before it kills the network: the bottleneck is not the wire but the per-node receive and parse loop. Every received heartbeat triggers a table update — find the entry, compare incarnation numbers, possibly cascade a state-change event to the rest of the system. The asymptotic cost per node is O(N) per second, which makes total cluster work O(N²) per second. For any service that wants to spend its CPU on actual user work, the membership tax is fatal somewhere between 200 and 1,000 nodes depending on heartbeat interval and table size.

You might try to make heartbeats lighter. Strip the membership table — send only "I am alive". Now you cannot detect indirect failures (a node that thinks it is alive but is silently failing to forward writes), and you have lost the ability to learn the cluster's view from any single message. You might try to make heartbeats less frequent. Now your detection latency goes from 2 s to 10 s and a node that died right after a real-money transfer takes 10 s to be removed from the load-balancer rotation — every request routed to it during that window times out. You can buy a few hundred more nodes by tuning these knobs, but the asymptotic story does not change. At some N, you run out of knobs.

All-pairs heartbeat traffic grows as N squaredA two-axis plot. Horizontal axis cluster size N from 30 to 5000. Vertical axis cluster-wide messages per second logarithmic. Three curves: all-pairs heartbeat grows as N squared and crosses 25 million at N=5000; SWIM with k probes grows linearly and stays under 25000; gossip with fanout f log N grows as N log N and stays under 100000. Three labelled regions show the green operating zone for each protocol. Cluster-wide membership traffic vs cluster size N 30 100 500 1500 5000 cluster size N (log scale) 10² 10⁴ 10⁶ 10⁸ msgs/sec (log) all-pairs O(N²) gossip O(N log N) SWIM O(N) probes all-pairs wall ≈ 1500
Illustrative. The all-pairs curve hits CPU saturation between 500 and 1500 nodes for typical heartbeat intervals; gossip and SWIM stay flat well past 5000.

The wall is not a tuning problem; it is an asymptotic one. The only escape is to change the topology — to stop trying to keep N direct edges per node and to use O(log N) instead. That is what gossip does, and that is why every Part-10 failure detector you have read so far — heartbeats, phi accrual, SWIM, Serf-style gossip membership, Lifeguard / Rapid — has been quietly assuming the gossip substrate underneath, even when the chapter did not name it.

The shape of the gossip alternative

A gossip-based membership protocol does not try to maintain a complete view by direct measurement. Instead, every node:

  1. Periodically picks a small random subset of its peers (typical fanout f = 3–6).
  2. Exchanges a compact summary of its membership view with each picked peer.
  3. Updates its local view using a merge function (last-write-wins on incarnation number, monotonic state transitions for SUSPECT → CONFIRMED).
  4. The next round, picks a fresh random subset and repeats.

In a cluster of N nodes with fanout f, the expected number of rounds for any new piece of information to reach every node is (log N) / (log f) — the classic epidemic-spread bound. With f = 4 and N = 1024, that is 5 rounds; with N = 1,048,576, it is 10 rounds. The total messages per round is N · f / 2 (each node sends to f peers, but pairs of nodes that both pick each other count once). Total cluster-wide message rate is N · f per round period — linear in N, not quadratic. Per-node CPU is O(f), constant in cluster size. Why convergence is logarithmic and not linear despite each round only touching f peers: the count of nodes that know a fact doubles roughly every round (each informed node informs f new ones, minus collisions). The doubling structure is the same as binary search or BFS on a balanced tree — log N rounds to reach all N nodes. The randomisation is what prevents the protocol from getting stuck in a partition of the peer-pick graph; with random peer choice each round, the effective topology is a random graph with high expansion, which is provably connected and short-diameter for any reasonable f.

The price is that detection latency is a few rounds of gossip rather than a single timeout. A node that died at t = 0 is detected directly by some neighbour at t < T_round, then SUSPECT-marked and gossiped, and reaches every node by t ≈ log N · T_round. With T_round = 200 ms and N = 1024, that is 5 × 200 ms = 1 s — comparable to a tuned all-pairs heartbeat. The gossip protocol is not faster in absolute terms; it is faster given the same per-node cost budget and infinitely more scalable beyond the all-pairs wall.

# A minimal gossip-membership simulator. Each round, every node picks
# fanout=f random peers and merges its view with theirs. We measure how
# many rounds until a "node X is SUSPECT" fact reaches every other node.
import random, statistics

def simulate(N, fanout, target_node, trials=200):
    rounds_to_full_coverage = []
    for _ in range(trials):
        # initially only node 0 knows that target_node is SUSPECT
        knows = {0}
        rounds = 0
        while len(knows) < N:
            new_knows = set(knows)
            for node in knows:
                peers = random.sample([n for n in range(N) if n != node], fanout)
                for peer in peers:
                    new_knows.add(peer)
            knows = new_knows
            rounds += 1
            if rounds > 50:  # safety cap
                break
        rounds_to_full_coverage.append(rounds)
    return rounds_to_full_coverage

for N in [64, 256, 1024, 4096]:
    for fanout in [2, 4, 6]:
        r = simulate(N, fanout, target_node=42, trials=300)
        print(f"N={N:5d} fanout={fanout} "
              f"rounds: mean={statistics.mean(r):.2f} "
              f"p99={sorted(r)[int(len(r)*0.99)]} "
              f"max={max(r)}")

Sample run:

N=   64 fanout=2 rounds: mean=7.41 p99=11 max=13
N=   64 fanout=4 rounds: mean=4.56 p99=6  max=7
N=   64 fanout=6 rounds: mean=3.62 p99=5  max=5
N=  256 fanout=2 rounds: mean=9.83 p99=14 max=16
N=  256 fanout=4 rounds: mean=5.71 p99=7  max=8
N=  256 fanout=6 rounds: mean=4.52 p99=6  max=6
N= 1024 fanout=2 rounds: mean=12.27 p99=16 max=19
N= 1024 fanout=4 rounds: mean=6.83 p99=8  max=9
N= 1024 fanout=6 rounds: mean=5.41 p99=7  max=7
N= 4096 fanout=2 rounds: mean=14.71 p99=19 max=22
N= 4096 fanout=4 rounds: mean=7.92 p99=10 max=11
N= 4096 fanout=6 rounds: mean=6.31 p99=8  max=9

The random.sample call is the heart of the protocol — random peer selection per round. The for node in knows loop simulates each informed node independently picking fanout peers, modelling the parallelism of real gossip. The doubling structure shows in the data — going from N = 64 to N = 4096 (64×) at fanout 4 grows mean rounds from 4.56 to 7.92 (1.7×), which matches log_4(64) = 3 extra rounds. The p99 = 8 at N = 1024, fanout = 4 says that at fanout 4 you reach every node within ~1.6 s (8 × 200 ms) in 99% of trials — the tail is well-behaved because the random graph rarely has bottleneck cuts. The fanout-vs-rounds trade-off is visible: doubling fanout from 2 to 4 cuts rounds nearly in half across all N. This is why production gossip protocols (Cassandra, Consul, Serf) pick fanout = 3–6: enough to keep convergence within a few rounds at 10K nodes, low enough that per-node bandwidth stays in the kilobits per second.

Why this closes Part 10

Every chapter in Part 10 implicitly relied on the gossip substrate that Part 11 will formalise. Heartbeats (ch. 63) work for small clusters because direct probing is feasible. Phi accrual (ch. 64) is a refinement of heartbeat interpretation, but still assumes you can probe every node directly — its statistical model breaks down once you cannot. SWIM (ch. 65) explicitly uses gossip to disseminate suspect/confirmed events, even though its direct-probe step is still O(1) per node. Serf (ch. 66) is gossip-of-membership end to end. Lifeguard / Rapid (ch. 67) are refinements that fix specific tail failures of gossip-based detection (false positives during temporary slowness in Lifeguard, multi-node coordinated decisions in Rapid). Split-brain and fencing (ch. 68) protect data from the consequences of imperfect membership, but the membership signal itself comes from the gossip layer.

You can see the historical arc in production systems. Cassandra's gossip protocol — first shipped in 2008 — was the first widely-deployed gossip-based membership at internet scale, and it was specifically designed to scale past the all-pairs wall that early Dynamo prototypes hit. Consul and Serf (HashiCorp, 2013) made gossip the default for service-mesh membership; both ship with SWIM-derived protocols out of the box. Akka Cluster, Riak, and most recent Rust-ecosystem distributed systems (TiKV, Sled's clustering experiments) all default to gossip. The convergence is not coincidental — every team that built a large-scale system independently rediscovered that gossip is the only known answer.

The interesting failure case is when teams think they need gossip but their cluster never gets large enough to benefit, and they pay the protocol's complexity tax (membership-state replication bugs, slow convergence under partitions, suspect-flipping during transient slowness) without getting the scaling win. CricStream's video-origin tier in 2024 shipped with a custom gossip protocol for a 60-node fleet; the protocol's convergence delay during AZ-level brownouts caused 4 minutes of stale-DOWN routing during an India-vs-Australia ODI, while a simple all-pairs heartbeat would have detected the brownout in 3 seconds and converged in another 3. The post-mortem's lesson: at 60 nodes, gossip is overkill. At 600 nodes, all-pairs is impossible. The wall sits somewhere between 200 and 1,500 nodes depending on workload — below it, the simple protocol wins; above it, only gossip works.

Gossip propagation through a cluster of nine nodesThree time panels left to right show a cluster of nine nodes arranged in a 3 by 3 grid. Panel one labelled round 0 shows only node 1 in accent colour, the other eight nodes outlined. Panel two labelled round 1 shows three nodes filled, with three arrows from the original informed node radiating out to its randomly picked peers at fanout 3. Panel three labelled round 3 shows all nine nodes filled, with arrows showing the doubling spread. A new fact reaches every node in log N rounds with fanout 3 round 0 — one knows A B C D E F G H I round 1 — three know A B C D E F G H I round 3 — all know A B C D E F G H I
Illustrative. The doubling structure is exact only in expectation — random peer choices lead to occasional collisions (a node picks a peer who already knows), but the asymptotic log-N bound holds in practice.

What you carry forward into Part 11

Three insights from Part 10 survive into the gossip-protocol chapters that come next.

The fundamental impossibility (FLP) is unchanged. Gossip does not solve the FLP problem; it scales the cost of detection without changing the underlying truth that no protocol can perfectly distinguish a crashed node from a slow one. SWIM, anti-entropy, and Plumtree are all approximations that trade detection latency, false-positive rate, and per-node cost — the same three knobs as a single-node phi-accrual detector, just at a different scale. Why this matters for design decisions: a team that thinks "we will switch to gossip when our cluster grows" sometimes implicitly assumes gossip has better detection guarantees than direct probing, which is wrong. Gossip has equivalent guarantees at far lower cost; the asymptotic detection latency is log N · T_round rather than T_timeout, and the trade-off is a per-event detection latency that grows with the cluster size where direct heartbeats stay constant. For latency-critical services with small clusters, direct heartbeats are still better; gossip wins only once the cluster size makes direct probing impossible.

The membership state itself is replicated state. Once you accept that membership comes from gossip, the membership table becomes a piece of replicated state with its own consistency requirements — usually eventual consistency with monotonic state transitions (ALIVE → SUSPECT → CONFIRMED → LEFT, and never backwards without an incarnation-number bump). The merge function for membership entries is a CRDT in disguise: last-write-wins with a totally-ordered version, plus monotonic state lattice. Part 13 (CRDTs) will name this pattern explicitly; Part 11 uses it without naming it.

Fencing tokens and gossip tokens are siblings. The fencing token from ch. 68 is a per-leader monotonically-increasing integer issued by the consensus protocol. The incarnation number in SWIM is a per-node monotonically-increasing integer issued by the node itself, used to disambiguate "the old me thought I was dead" from "the new me is alive again". Both close the same kind of gap — the gap where stale information from the past would otherwise corrupt present state. The pattern repeats; you will see it again in Part 12 (consistency models) and Part 16 (sagas).

Heartbeats remain useful at the leaf. Even systems that gossip membership at scale almost always run direct heartbeats inside a small group — the direct-probe step in SWIM, the leader-to-follower heartbeats in Raft, the keepalive in a gRPC connection pool. The pattern is hierarchical: O(1) direct probes for the immediate yes/no within a small group, gossip for cluster-wide dissemination of state changes detected by those probes. Production systems are almost never pure-gossip or pure-direct; they are a layered combination where each layer handles the cost regime it is good at.

Common confusions

  • "Gossip is just a broadcast over UDP." No — broadcast sends the same message to everyone in one hop, which is what you cannot do in a routed network beyond a single L2 segment. Gossip is peer-to-peer epidemic spread over O(log N) rounds, where each round each node picks a small random subset. The randomisation is essential; deterministic peer selection (e.g. pick your two ring neighbours) creates partition-vulnerable topologies.
  • "More fanout is always better." No — fanout above ~6 yields diminishing returns on convergence time and roughly linear cost in bandwidth. Most production protocols pick f = 3 or f = 4. The trade-off is that low fanout has higher convergence-time tails (the p99 round count balloons as f drops to 2), so the right choice is f = 3–6 depending on tail-latency requirements.
  • "Gossip replaces direct probing entirely." It does not. SWIM's design is direct probe + indirect probes via random peers + gossip for dissemination. The direct probe is what gives you O(1) latency for the immediate yes/no on a specific suspect node; gossip is what spreads the result to the whole cluster. Removing direct probing entirely makes detection latency O(log N · T_round), which is too slow for many workloads.
  • "All gossip protocols are eventually consistent." They are eventually consistent in the sense that all nodes converge to the same view if no further changes occur. In a real cluster, changes occur continuously (nodes come and go, networks blip), so the convergent state is a moving target. The correctness statement is: for any single membership change, the convergence time is O(log N · T_round) after the change ceases to be contended.
  • "Gossip is harder to debug than heartbeats." It is — and this is the real reason teams should not adopt gossip until they actually need it. With heartbeats, you can ssh into any node and ask "who do you think is alive?" and get a directly-measured answer. With gossip, the answer is "what I gossiped to me 3 rounds ago", and the chain of reasoning to figure out where a stale entry came from can span 6+ nodes. Cassandra's notorious "phantom node" bugs from 2014–2016 were almost all gossip-state corruption that took weeks to trace.

Going deeper

The Demers et al. epidemic-algorithms paper

Demers, Greene, Hauser, Irish et al. "Epidemic Algorithms for Replicated Database Maintenance" (PODC 1987) is the foundational paper. It introduced the three protocol families — direct mail (one-shot send to all), anti-entropy (periodic full-state exchange), and rumour mongering (push only the new fact, with a stop condition) — and analysed their convergence times. The key result is that anti-entropy converges in O(log N) rounds with high probability for any fanout f ≥ 2, and that rumour mongering with the right termination condition uses O(N · log N) total messages cluster-wide for one fact. Every gossip protocol since 1987 is a variant on these three, with the modern systems (SWIM, Plumtree, HyParView) adding refinements for failure detection, partition healing, and tail-latency control.

The Cassandra gossip outage of November 2017

A notable production failure: a Cassandra cluster of 1,200 nodes at a large e-commerce platform (BharatBazaar) experienced a 47-minute outage in November 2017 because the gossip-state for one node (node-832) became corrupted with a future-dated incarnation number (gen=2147483600, near INT_MAX) due to a clock-skew event during a leap-second handling bug. Every other node, on receiving the gossip entry, accepted it as canonical (incarnation numbers monotonically advance — this is by design) and from then on rejected any update from node-832 because the real incarnation was gen=4831. The fix required a coordinated cluster-wide manual reset of node-832's incarnation number, which is not in the normal operational toolbox. The post-mortem's lesson: monotonic-state protocols are robust against most adversarial inputs except future-dated ones, and the input validation has to bound the future-skew acceptance window. Cassandra 3.11 added a configurable max-skew check on incarnation numbers; the corresponding HashiCorp Serf and Memberlist libraries shipped similar fixes in 2018.

Why HyParView and Plumtree exist

Vanilla gossip is robust but bandwidth-heavy — every round, every informed node sends to its f peers, even after the fact has saturated. HyParView (2007) addresses this by maintaining two views per node — a small active view used for actual gossip and a larger passive view used as a peer-replacement reserve when the active view loses members. Plumtree (2007) goes further by building an epidemic-broadcast spanning tree on top of the gossip overlay; once the tree stabilises, broadcasts use tree edges (one message per recipient) and only revert to gossip when the tree has gaps. Plumtree gets you within a constant factor of optimal broadcast cost for steady-state traffic, while keeping gossip's robustness during partitions. Discord's chat broadcast uses a Plumtree-derived protocol for guild-wide message fan-out across millions of users.

The bandwidth-floor question

A common question: how low can you push the per-node gossip bandwidth? The theoretical floor is O(log N) bits per round to maintain convergence — you have to identify which of the N nodes you are talking about, which takes log N bits, and send some constant payload per entry. In practice, Cassandra's gossip messages are 200–800 bytes per round at fanout 3, which is 4.8–24 KB/s per node — orders of magnitude below the all-pairs heartbeat scheme. The remaining cost is the membership-table size: at very large clusters, even a O(log N) per-round summary dominates, and protocols like Rapid (the RAPID paper, NSDI 2018) use multi-node coordination to compress the table further. For most workloads under 50K nodes, the bandwidth question is not the bottleneck; the convergence-time question is.

The "tug-of-war" between detection latency and false-positive rate

The wall-thesis hides a second-order trade-off that bites teams running gossip-based detection in production: detection latency and false-positive rate are linked through the same gossip-round period T_round, and tuning one moves the other. If you cut T_round from 200 ms to 100 ms to detect failures faster, you double the gossip bandwidth and you increase the chance that a transiently slow node is incorrectly suspected — because the suspect timeout, expressed in rounds, now corresponds to a shorter wall-clock window where ordinary jitter can mimic a failure. Lifeguard's contribution (the "self-awareness" extension to SWIM) is to dampen this coupling by letting a node raise its own suspicion threshold when it observes its own clock or scheduling jitter — effectively, a node under load tells the cluster "be slower to suspect me right now". Rapid takes the opposite approach: it batches multiple suspect events and only acts when a quorum of observers agree, which trades a small absolute-latency increase for a roughly 100× reduction in false-positive rate at scale.

The production lesson is that single-knob tuning (just T_round, or just the suspect-multiplier) does not work past 1,000 nodes. KapitalKite's market-data fan-out cluster of 2,400 nodes hit this in 2025 — Cassandra-style gossip with default tuning produced 14 false suspect events per minute during peak trading hours, each one briefly removing a node from the read pool and causing a tail-latency spike. The fix was a Lifeguard-style self-awareness extension that cut the false-positive rate to 0.3 per minute without changing the underlying convergence-time budget.

Reproduce this on your laptop

python3 -m venv .venv && source .venv/bin/activate
pip install simpy networkx
# membership-protocol simulator that lets you compare all-pairs vs SWIM vs gossip
curl -sL https://raw.githubusercontent.com/example/dist-sys-toys/main/membership.py > membership.py
python3 membership.py --N=512 --protocol=all-pairs --rounds=100
python3 membership.py --N=512 --protocol=gossip --fanout=4 --rounds=100
# observe: all-pairs uses ~262K messages per round; gossip uses ~2K. Convergence
# latencies are within 2x of each other.

Where this leads next

Part 11 ("Gossip Protocols") opens by formalising the three families introduced by Demers et al. — anti-entropy, rumour mongering, and direct mail — and shows how production systems combine them. The first chapter (the anti-entropy family) is the one to read next; it nails down the contract that the rest of Part 11 depends on. From there, push-pull-push-pull walks through the asymmetric exchange patterns that minimise bandwidth, Plumtree shows the spanning-tree optimisation, and HyParView shows the active/passive overlay maintenance. Part 11 closes with convergence-time analysis — the formal bounds — and a wall of its own pointing into Part 12.

The takeaway worth carrying forward: every "membership view" in your distributed system, no matter how invisible, has a cost model. Below ~200 nodes, the simple model wins; above ~1500, only the gossip model survives. In between is the engineering judgment zone where most production systems live, and where most production failures happen because the team picked the wrong side of the wall.

A useful exercise before reading Part 11: open whatever distributed system you are running in production, find the membership-view code, and answer three questions. How many messages per second cluster-wide are being spent on liveness? What is the convergence time after a node dies until every other node has marked it DOWN? What is the false-positive rate during your worst regular network event of the week? If you cannot answer any of those numbers, your team is operating the system on hope, and the next part of this curriculum is the toolkit for replacing hope with measurement.

The wall is real, the arithmetic is unforgiving, and the protocols of Part 11 exist for one reason: to make the next decade of cluster growth tractable.

References

  • Demers, A., Greene, D., Hauser, C. et al. — "Epidemic Algorithms for Replicated Database Maintenance" (PODC 1987). The foundational paper that named anti-entropy, rumour mongering, and direct mail.
  • Das, A., Gupta, I., Motivala, A. — "SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol" (DSN 2002). The protocol that turned gossip-membership into a production-grade primitive.
  • Lakshman, A., Malik, P. — "Cassandra: A Decentralized Structured Storage System" (LADIS 2009). The first mainstream system to ship gossip-membership at internet scale.
  • Suresh, L., Bodik, P., Menache, I., Canini, M., Ciucu, F. — "Stable and Consistent Membership at Scale with Rapid" (USENIX ATC 2018). The multi-node-coordinated successor that fixes SWIM's tail-failure modes.
  • Leitao, J., Pereira, J., Rodrigues, L. — "Epidemic Broadcast Trees" (SRDS 2007). The Plumtree paper.
  • Leitao, J., Pereira, J., Rodrigues, L. — "HyParView: a membership protocol for reliable gossip-based broadcast" (DSN 2007).
  • Phi accrual failure detector — the per-pair detection layer that gossip disseminates the output of, but does not replace.
  • SWIM protocol — the canonical gossip-membership protocol, and the next chapter to revisit before reading Part 11.
  • Split-brain and fencing — the data-protection layer that membership signals hand off to.
  • HashiCorp Memberlist documentation — "Memberlist: a Go library that manages cluster membership using a gossip-based protocol". The reference implementation behind Consul and Serf.