Engineering Atlas › System Design Fundamentals

Choosing a leader among nodes

13 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Why you need a leader

A single leader turns a coordination problem into a local decision. The cost is a throughput ceiling and a failover gap when it dies.

Classic algorithms

The Bully algorithm has each node with a known ID; a node that suspects the leader is dead messages all higher-ID nodes, and if none reply it declares itself leader. The ring algorithm passes an election message around a logical ring collecting IDs, then announces the highest. Both assume reliable failure detection, so a partition gives each side its own leader. They are teaching tools, not production designs. Google's SRE book tells the production version of this story: a pair of file servers that picked a master by heartbeat timeout and STONITH ended up either both active or both down when the network degraded, because they tried to solve leader election with simple timeouts (Google SRE book).

Consensus-based election

Raft and ZooKeeper's Zab elect leaders by majority vote within a numbered term. Two leaders in the same term are impossible because each voter votes once per term and majorities overlap. A deposed leader from an older term can still be alive and acting, but any write it sends will be rejected by a majority that has moved to the newer term.

Lease-based election on a coordination service

Most applications borrow consensus. Each candidate tries to create a key such as /services/billing/leader with a TTL of, say, 10 s, using a compare-and-set that succeeds only if the key is absent. The winner renews every 3 s. Others watch the key and try again when it disappears. ZooKeeper does the same with ephemeral sequential znodes: the lowest sequence number leads, and each node watches only its predecessor to avoid a thundering herd. Kubernetes controllers use a Lease object with leaseDurationSeconds and renewTime (Kubernetes docs). Kubernetes also shows that "who leads" can be a policy, not a race: coordinated leader election (beta since v1.33, off by default) has candidates register LeaseCandidate objects, and the only built-in strategy picks the candidate with the oldest emulation version, so the leader during a rolling upgrade is always the one every peer is compatible with (Kubernetes docs).

Fencing

Suppose the leader stalls in a 15 s GC pause, which is not far-fetched: stop-the-world pauses have been known to last minutes (Kleppmann). Its 10 s lease expires, another node takes over, then the old leader wakes up and finishes a write it started. The lease alone cannot stop this. Fix it by issuing a monotonically increasing token with each lease (etcd's revision, ZooKeeper's zxid, or a counter you bump). Every write carries the token, and the storage layer rejects any token lower than the highest it has seen.

Self-demotion

A leader should stop acting before its lease could expire at the store. If the lease is 10 s, step down at about 7 s without a successful renewal. The margin absorbs clock rate drift and message delay. Amazon's guidance adds two habits: check the remaining lease time right before any side effect outside the leader, and do not renew from a background thread that keeps the lease alive while the work thread is stuck (AWS Builders' Library).

Down the stack

Layer 1The contract

There are two different promises, and mixing them up causes most incidents. Consensus election promises at most one leader per term; it never promises that only one process believes it is leader, because a deposed leader may not have heard yet. A lease promises that the store grants the key to one holder at a time, and that the holder can safely act for ttl - margin measured on its own monotonic clock, provided clock rates drift within a bound and the process is not paused. Only fencing gives a promise that holds with no timing assumption: the protected resource never accepts a write from an older term. Liveness: after the leader dies, a new one is in place within about one lease duration plus one retry period.

Layer 2The mechanism

ZooKeeper's own election (Zab's fast leader election) has each server vote for the peer with the highest (epoch, zxid, serverId), so the winner always has the most complete history. A zxid is 64 bits: the high 32 bits are the leader epoch and the low 32 a counter, so every transaction id also encodes which leadership produced it, which is why it works as a fencing token (ZooKeeper internals). Applications usually elect with the recipe above: ephemeral sequential znodes under a parent, lowest sequence wins, each candidate watching only its predecessor. The ephemeral node is bound to a session that the client keeps alive with pings; the server clamps the requested session timeout to between 2 and 20 times tickTime, which the sample config sets to 2 s (ZooKeeper admin guide).

etcd's concurrency.Election follows the same pattern with leases: each candidate writes a key under a prefix attached to its lease, and the key with the lowest create revision leads. Keepalives travel over a long-lived gRPC stream. Kubernetes' client-go elector stores holder identity and renewTime in a Lease object and updates it with an optimistic-concurrency write on resourceVersion. Controller-manager's defaults are a 15 s lease, a 10 s renew deadline and a 2 s retry period, so the leader gives up after 10 s without a renewal, leaving 5 s of margin (kube-controller-manager reference). Google's Chubby handed out sequencers, an early form of fencing token, and told a newly elected primary to confirm with CheckSequencer() that it was still primary (Chubby paper).

Layer 3The machine

Leases must be timed with CLOCK_MONOTONIC, never wall-clock time; Amazon's leases rely only on locally measured elapsed time for this reason (AWS Builders' Library): NTP can step the wall clock by seconds, while the monotonic clock only drifts with the crystal, typically tens of parts per million, or a few hundred microseconds over a 10 s lease. Drift is not the real threat. Pauses are: a stop-the-world GC on a large heap, a VM live migration, a swap storm or a SIGSTOP can freeze a process for seconds, and when it resumes the next instruction may be a write. No pre-write check closes that window.

The storage side enforces fencing cheaply: a conditional update such as UPDATE jobs SET ... WHERE fence <= $token is one indexed comparison inside a write that already happens. Each renewal is a small RPC but also a consensus write in etcd or ZooKeeper, so it costs a WAL fsync on a quorum. Thousands of short leases renewed every second can saturate the coordination cluster's disks.

Why it's built this way

  1. Machines cannot tell a dead peer from a slow one, so any election is driven by timeouts and will sometimes produce two processes that think they lead.
  2. Electing by majority prevents two leaders in one term, but it cannot stop an old leader acting late, so a term number must travel with every write.
  3. Running consensus in every application is costly and error-prone, so applications borrow it from a small coordination cluster and hold leases there.
  4. Leases need time, and only local monotonic durations are trustworthy, so expiry is measured locally with a safety margin and backed by fencing at the resource.

Diagram

sequenceDiagram
    participant A as Node A
    participant S as Lock store
    participant B as Node B
    participant D as Storage
    A->>S: acquire lease, TTL 10s
    S-->>A: granted, token 41
    A->>D: write with token 41
    Note over A: GC pause 15s
    B->>S: acquire lease
    S-->>B: granted, token 42
    B->>D: write with token 42
    A->>D: late write with token 41
    D-->>A: rejected, stale token

Worked example

A billing reconciliation job runs on 3 replicas; exactly one should run at a time. You use an etcd lease with TTL 10 s and renew every 3 s.

If the job writes to a Postgres table, add a leader_epoch column check: UPDATE ... WHERE id = $1 AND $token >= (SELECT max_epoch FROM fence). Now a stale leader's late write affects zero rows.

At scale, prefer one lease per shard rather than one global leader. With 1,000 shards and 50 workers, each worker holds about 20 leases, and a single failure moves 20 shards, not everything.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Lease in etcd/ZooKeeper/ConsulBattle-tested, watches, fencing tokensAnother tier-0 dependencyMost services needing a single active worker
Database row leaseNo new infrastructurePolling, DB failover affects leadershipYou already have a strongly consistent DB
Embedded RaftNo external dependency, leader is also the data ownerYou operate consensus yourselfYou are building a replicated data store
Bully / ringSimple, no external serviceUnsafe under partitionsTeaching, or fixed clusters with fencing elsewhere
Cloud primitive (e.g. Kubernetes Lease)Native to the platformBound to the API server's availabilityControllers and operators on Kubernetes

Pitfalls in production

Leadership lens

Check yourself

  1. Why does a lease not by itself guarantee a single active leader?

    Show answer

    A paused or partitioned leader cannot know its lease has expired and may keep acting. Only the resource checking a fencing token can reject its late writes.

  2. Why should a leader step down before its lease TTL elapses?

    Show answer

    Its local timer and the store's timer can drift and messages take time, so a safety margin keeps it from acting after another node has been granted the lease.

  3. Why does ZooKeeper's recipe have each node watch only its predecessor?

    Show answer

    If all nodes watched the leader's znode, every one would wake on its deletion and race, creating a herd. Watching the predecessor wakes only the next in line.

  4. What failure does the Bully algorithm not handle?

    Show answer

    A network partition: each side sees the other as dead and elects its own leader.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →