How the PreVote Mechanism in Dragonboat Prevents Split-Brain Scenarios

The PreVote mechanism in Dragonboat acts as a dry-run election that verifies quorum feasibility without incrementing the term, preventing stale or partitioned nodes from forcing the current leader to step down and causing split-brain.

Dragonboat is a high-performance Raft consensus implementation in Go. The PreVote mechanism in dragonboat extends the standard Raft algorithm to add a preliminary election phase that protects cluster stability. This mechanism, derived from the Raft thesis §5.2, ensures that nodes cannot disrupt an active leader unless they first prove they can reach a majority of the cluster.

How PreVote Works in Dragonboat

Triggering a PreVote Campaign

When a follower’s election timeout expires, the node invokes handleNodeElection in internal/raft/raft.go. If the configuration has PreVote enabled and the node is not currently a leader-transfer target, it initiates a pre-vote campaign instead of a normal election.

// internal/raft/raft.go
// L1081-L1087
if r.preVote && !r.isLeaderTransferTarget {
    plog.Debugf("%s will start a preVote campaign", r.describe())
    return r.preVoteCampaign()
}

This check ensures that leadership transfers are not delayed by the pre-vote phase.

Becoming a PreVote Candidate

The preVoteCampaign method transitions the node to the preVoteCandidate state. Crucially, this transition does not increment the term. The node resets its election timer and clears any prior vote, but its term remains unchanged.

// internal/raft/raft.go
// L1001-L1018
r.state = preVoteCandidate
r.reset(r.term, true)   // term stays the same
r.setLeaderID(NoLeader)
plog.Warningf("%s became PreVote candidate", r.describe())

This temporary state allows the node to test its eligibility without affecting the cluster’s term.

Broadcasting RequestPreVote Messages

The pre-vote candidate broadcasts RequestPreVote messages to all voting members. These messages include the candidate’s last log index and term, allowing recipients to verify the candidate’s log is at least as up-to-date as their own. Notably, the message term is set to term + 1 for the request only, but the sender’s local term remains unchanged.

// internal/raft/raft.go
// L1150-L1174
r.send(pb.Message{
    Term:     term + 1,               // term is *one* higher only for the request
    To:       k,
    Type:     pb.RequestPreVote,
    LogIndex: index,
    LogTerm:  lastTerm,
})
plog.Warningf("%s sent RequestPreVote to %s", r.describe(), ReplicaID(k))

Evaluating PreVote Requests

When a peer receives a RequestPreVote, it executes handleNodeRequestPreVote. The peer grants the pre-vote only if two conditions are met: the incoming term is greater than the local term, and the candidate’s log is up-to-date (r.log.upToDate). If either check fails, the peer rejects the request without changing its own term.

// internal/raft/raft.go
// L1670-L1686
if m.Term > r.term && isUpToDate {
    resp.Term = m.Term
    // grant pre-vote
} else {
    resp.Term = r.term
    resp.Reject = true
}
r.send(resp)

This validation ensures that only viable candidates—those with fresh logs and higher terms—can proceed.

Transitioning to Real Election

The pre-vote candidate collects responses via handlePreVoteCandidateRequestPreVoteResp. If it receives a quorum of positive pre-votes, it invokes campaign() to start the actual election, which increments the term and sends RequestVote messages. If it fails to reach a quorum, it reverts to the follower state.

// internal/raft/raft.go
// L2260-L2274
count := r.handleVoteResp(m.From, m.Reject, true)
if count == r.quorum() {
    if err := r.campaign(); err != nil { return err }
} else if len(r.votes)-count == r.quorum() {
    r.becomeFollower(r.term, NoLeader)
}

This gated transition prevents term inflation and ensures that only nodes capable of reaching a majority can disturb the current leader.

Why PreVote Prevents Split-Brain

Split-brain occurs when two nodes simultaneously believe they are the leader, often because a partitioned or stale node increments its term and forces the legitimate leader to step down. Dragonboat’s pre-vote eliminates this risk through a dry-run validation phase.

Split-brain cause How pre-vote stops it
A stale follower increments its term, forces the current leader to step down, and wins a separate election → two leaders. The stale node does not increment its term during pre-vote. Its RequestPreVote is rejected because its log is not up-to-date, so it never becomes a candidate.
Multiple partitioned nodes each start an election at the same time, each gathers a minority of votes, and all increment terms → rapid term churn and possible split. Pre-vote ensures a node only proceeds to the costly term-increment step after it knows it can obtain a majority, thus avoiding unnecessary term changes that would disrupt the existing leader.
A node with a higher term but no quorum may keep sending RequestVote, causing other nodes to step down repeatedly. onMessageTermNotMatched (see handleNodeElection) drops RequestVote from a higher-term node if the local node still has a valid leader and the election timeout has not elapsed, further protecting the cluster from oscillations.
Leader transfer can be interfered with by a concurrent election. The pre-vote is skipped when a leadership transfer is in progress (r.isLeaderTransferTarget), so the transfer can complete without being pre-empted.

By requiring a node to prove it can reach a quorum before incrementing its term, Dragonboat ensures that only viable candidates can trigger leadership changes, maintaining single-leader safety even during network partitions.

Enabling and Configuring PreVote

PreVote is controlled via the Config struct. By default, Dragonboat enables this feature, but you can explicitly configure it when initializing a NodeHost.

cfg := config.Config{
    // Other configuration options...
    PreVote: true,   // Enable the pre-vote mechanism
}
nh, err := dragonboat.NewNodeHost(cfg)

(Source: config/config.go)

When enabled, the Raft state machine automatically uses the pre-vote protocol for all election timeouts, unless the node is the target of a leader transfer.

Summary

  • The PreVote mechanism in dragonboat adds a preliminary dry-run election phase before actual term increments.
  • Nodes in the preVoteCandidate state broadcast RequestPreVote messages without increasing their term, testing quorum feasibility.
  • Peers reject pre-vote requests from stale or partitioned nodes via handleNodeRequestPreVote, preventing term inflation.
  • Only after receiving a quorum of positive pre-votes does the node call campaign() to increment its term and start a real election.
  • This protocol eliminates split-brain scenarios by ensuring only nodes with up-to-date logs and reachable quorums can disturb the current leader.

Frequently Asked Questions

What is the difference between PreVote and a regular Raft election?

A regular Raft election immediately increments the candidate’s term and sends RequestVote messages. In contrast, the PreVote mechanism in dragonboat first enters a preVoteCandidate state where it sends RequestPreVote messages using a tentative term (local term + 1) without actually incrementing its local term. Only if it receives a quorum of positive responses does it proceed to the real election. This prevents stale nodes from disrupting the cluster with spurious term increments.

How does PreVote interact with leader transfer operations?

Dragonboat explicitly disables PreVote when processing a leader transfer. In handleNodeElection, the code checks r.isLeaderTransferTarget before initiating a pre-vote campaign. If a leader transfer is in progress, the node skips the pre-vote phase and proceeds directly to the regular election mechanism. This ensures that leadership transfers complete quickly without being delayed by the preliminary quorum check.

Can PreVote be disabled, and when should you consider disabling it?

Yes, you can disable PreVote by setting PreVote: false in the config.Config struct when creating the NodeHost. However, this is generally not recommended for production deployments. Disabling PreVote may be useful in specific testing scenarios where you need to observe classic Raft behavior, or in extremely small clusters (e.g., two nodes) where the additional network round-trip of pre-vote adds unnecessary latency. For most production clusters of three or more nodes, leaving PreVote enabled provides essential protection against split-brain scenarios.

Does the PreVote mechanism affect write latency or throughput?

The PreVote mechanism adds one network round-trip (the exchange of RequestPreVote and RequestPreVoteResp messages) only during the election phase. Since elections are rare events in a stable cluster—occurring only when the leader fails or network partitions occur—PreVote has negligible impact on steady-state write latency and throughput. The trade-off of one additional round-trip during elections is overwhelmingly beneficial compared to the potential downtime and inconsistency caused by a split-brain scenario.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →