hyperledger/fabric · error

ElectionTick ( ) must be greater than HeartbeatTick ( )

Error message

ElectionTick (%d) must be greater than HeartbeatTick (%d)

What it means

In etcd/raft, ElectionTick must be strictly greater than HeartbeatTick; otherwise heartbeats could trigger elections or elections would never time out properly. VerifyConfigMetadata enforces this ordering and echoes both values in the message.

Solutions

  1. Set ElectionTick strictly greater than HeartbeatTick (Fabric default: HeartbeatTick 1, ElectionTick 10).
  2. Submit a normal channel config update with corrected Options; note ElectionTick changes require careful liveness consideration.
  3. Pre-validate with VerifyConfigMetadata before submitting the update.

Example fix

// before
Options{HeartbeatTick: 10, ElectionTick: 5}
// after
Options{HeartbeatTick: 1, ElectionTick: 10}
Defensive patterns

Strategy: validation

Validate before calling

o := metadata.GetOptions()
if o.GetElectionTick() <= o.GetHeartbeatTick() {
	return fmt.Errorf("ElectionTick(%d) must exceed HeartbeatTick(%d)", o.GetElectionTick(), o.GetHeartbeatTick())
}

Try / catch

if err := VerifyConfigMetadata(meta, opts); err != nil {
	if strings.Contains(err.Error(), "must be greater than HeartbeatTick") {
		return fmt.Errorf("invalid tick ratio in config update: %v", err)
	}
	return err
}

Prevention

When it happens

Trigger: A config update sets Options.ElectionTick <= Options.HeartbeatTick (e.g. both 1, or ElectionTick 2 with HeartbeatTick 5).

Common situations: Hand-edited channel config where someone tuned ticks and inverted the relationship; copying tick values between profiles; confusion about tick semantics when migrating from Kafka.

Related errors


AI-assisted analysis of hyperledger/fabric@2736b63f8f (2026-09-04). Data as JSON: /api/errors/a0e4d35db37a45be. Report an issue: GitHub.

Appendix: source

Thrown at orderer/consensus/etcdraft/util.go:227

		// should always be called with non-nil config metadata
		return errors.Errorf("nil Raft config metadata")
	}

	if metadata.GetOptions() == nil {
		return errors.Errorf("nil Raft config metadata options")
	}

	if metadata.GetOptions().GetHeartbeatTick() == 0 ||
		metadata.GetOptions().GetElectionTick() == 0 ||
		metadata.GetOptions().GetMaxInflightBlocks() == 0 {
		// if SnapshotIntervalSize is zero, DefaultSnapshotIntervalSize is used
		return errors.Errorf("none of HeartbeatTick (%d), ElectionTick (%d) and MaxInflightBlocks (%d) can be zero",
			metadata.GetOptions().GetHeartbeatTick(), metadata.GetOptions().GetElectionTick(), metadata.GetOptions().GetMaxInflightBlocks())
	}

	// check Raft options
	if metadata.GetOptions().GetElectionTick() <= metadata.GetOptions().GetHeartbeatTick() {
		return errors.Errorf("ElectionTick (%d) must be greater than HeartbeatTick (%d)",
			metadata.GetOptions().GetElectionTick(), metadata.GetOptions().GetHeartbeatTick())
	}

	if d, err := time.ParseDuration(metadata.GetOptions().GetTickInterval()); err != nil {
		return errors.Errorf("failed to parse TickInterval (%s) to time duration: %s", metadata.GetOptions().GetTickInterval(), err)
	} else if d == 0 {
		return errors.Errorf("TickInterval cannot be zero")
	}

	if len(metadata.GetConsenters()) == 0 {
		return errors.Errorf("empty consenter set")
	}

	// verifying certificates for being signed by CA, expiration is ignored
	for _, consenter := range metadata.GetConsenters() {
		if consenter == nil {
			return errors.Errorf("metadata has nil consenter")
		}

View on GitHub (pinned to 2736b63f8f)