k3s-io/k3s · warning

this server has not yet been promoted from learner to voting

Error message

this server has not yet been promoted from learner to voting member

What it means

ETCD.Test() rejects a datastore health check when the member's status reports IsLearner: the server was added via MemberAddAsLearner and has not yet been promoted to a voting member. Learners cannot serve consensus, so the server is not yet a full member; promotion happens automatically once the learner catches up.

Source

Thrown at pkg/etcd/etcd.go:211

// Test ensures that the local node is a voting member of the target cluster
// and not in maintenance mode due to alarms.
// If it is still a learner or not a part of the cluster, an error is raised.
// If enableMaintenance is true, an attempt will be made to clear alarms.
// Startup defragmentation is delegated to etcd itself via the
// bootstrap-defrag-threshold-megabytes flag.
func (e *ETCD) Test(ctx context.Context, enableMaintenance bool) error {
	if e.config == nil {
		return errors.New("control config not set")
	}
	if e.client == nil {
		return errors.New("etcd datastore is not started")
	}

	status, err := e.status(ctx)
	if err != nil {
		return errors.WithMessage(err, "failed to get etcd status")
	} else if status.IsLearner {
		return errors.New("this server has not yet been promoted from learner to voting member")
	} else if status.Leader == 0 {
		return errorsv3.ErrNoLeader
	}

	logrus.Infof("Connected to etcd v%s - datastore using %d of %d bytes", status.Version, status.DbSizeInUse, status.DbSize)

	if len(status.Errors) > 0 {
		logrus.Warnf("Errors present on etcd cluster: %s", strings.Join(status.Errors, ","))
	}

	if !enableMaintenance {
		return nil
	}

	// clear alarms on this node
	if err := e.clearAlarms(ctx, status.Header.MemberId); err != nil {
		return errors.WithMessage(err, "failed to disarm etcd alarms")
	}

View on GitHub (pinned to 6ba341e396)

Solutions

  1. Treat as transient: wait and retry; promotion is automatic once the learner's raft log catches up.
  2. If stuck, check network/latency to peers and etcd health (member list shows isLearner flag); verify the cluster has quorum to promote.
  3. As last resort remove the stuck learner (etcdctl member remove) and rejoin the node fresh.
Defensive patterns

Strategy: retry

Validate before calling

// Poll member status until promotion before declaring the node ready:
func waitNotLearner(ctx context.Context, cli *clientv3.Client, memberID uint64) error {
	return wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(ctx context.Context) (bool, error) {
		ml, err := cli.MemberList(ctx)
		if err != nil { return false, nil }
		for _, m := range ml.Members {
			if m.ID == memberID { return !m.IsLearner, nil }
		}
		return false, nil
	})
}

Try / catch

if err := etcd.Test(ctx, false); err != nil {
	if strings.Contains(err.Error(), "not yet been promoted from learner") {
		// transient by design: backoff and re-Test; only page a human after N minutes
	}
}

Prevention

When it happens

Trigger: Immediately after adding a new server to an etcd cluster: the joiner sits as learner while syncing the datastore; Test() is called during startup before the catch-up completes; a stalled learner that never catches up never gets promoted.

Common situations: Normal (transient) during server joins, resolving in seconds-to-minutes; a chronically underprovisioned/slow network node stuck as learner; a learner added against a quorum-impaired cluster.

Related errors


AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15). Data as JSON: /api/errors/8674e9f98ca5d291. Report an issue: GitHub.