k3s-io/k3s · critical
failed to start etcd client
Error message
failed to start etcd client
What it means
k3s etcd package (pkg/etcd/etcd.go): the leader-elected '-etcd' controller startup asserts that an etcd client exists; on a control-plane node where normal etcd startup did not create one, it calls startClient, which dials the local etcd with client TLS certificates. A panic with this message means the embedded etcd could not be reached or the client could not be constructed (TLS material, connectivity, or etcd not serving yet).
Solutions
- Check embedded etcd health on the node: k3s etcd-snapshot... or curl the local endpoint and inspect k3s logs for etcd startup errors before this panic
- Verify etcd client TLS files exist and match the CA in ${datadir}/server/tls (etcd/client-ca.crt, etcd/server-client.crt/key) and regenerate if a restore rotated material
- If etcd is wedged after a failed restore, follow the tombstone/backup guidance: back up and remove ${datadir}/server/db or rejoin with --server
- Ensure quorum: on multi-node control planes confirm a majority of etcd members are up; on single-node just free resources (disk, memory) and restart k3s
Example fix
// before
if e.client == nil {
if err := e.startClient(ctx); err != nil {
panic(errors.WithMessage(err, "failed to start etcd client"))
}
}
// after (fail with context instead of panic when etcd is not yet reachable)
if e.client == nil {
if err := e.startClient(ctx); err != nil {
return fmt.Errorf("failed to start etcd client (is embedded etcd running on this node?): %w", err)
}
} Defensive patterns
Strategy: validation
Validate before calling
// before the leader-elected etcd controller runs, confirm the local etcd answers with the client certs
exec.Command("etcdctl", "--endpoints=https://127.0.0.1:2379",
"--cacert=${datadir}/server/tls/etcd/server-ca.crt",
"--cert=${datadir}/server/tls/etcd/server-client.crt",
"--key=${datadir}/server/tls/etcd/server-client.key",
"endpoint", "health").Run() Try / catch
// recover pattern for a panic thrown inside the leader-elected controller start func
func safeStart(ctx context.Context, name string, start func(context.Context)) {
defer func() {
if r := recover(); r != nil {
logrus.WithField("stack", string(debug.Stack())).Fatalf("%s controller panic: %v", name, r)
}
}()
start(ctx)
} Prevention
- Verify embedded etcd health and the ${datadir}/server/tls/etcd client certificate set after every restore or cert rotation
- Keep control-plane resources (disk, memory) within bounds so etcd can serve during controller startup
- On multi-node control planes, ensure an etcd quorum exists before restarting nodes so startClient does not race a down datastore
- Watch k3s logs for the tombstone-file guidance and follow it rather than forcing restarts on a removed member
When it happens
Trigger: Leader election won on a control-plane node whose local etcd is down or still starting (no listener on 127.0.0.1:2379); missing/expired/mismatched etcd client certs in ${datadir}/server/tls (client-ca, etcd-client); etcd data dir corruption or permission problems; cluster restored from snapshot with stale certificates.
Common situations: Restarting k3s immediately after a crash where etcd is slow to gain quorum; certificate rotation or restore-from-backup leaving etcd client certs inconsistent; disk-full preventing etcd from serving; mixed k3s versions on control-plane nodes during upgrades.
Related errors
- failed to start leader controller
- failed to start wrangler controllers
- cannot use S3 config secret when restoring snapshot…
- duplicate node name found, please use a unique name for…
- etcd datastore disabled
AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15).
Data as JSON: /api/errors/84998891b72002a2.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/etcd/etcd.go:664
})
}
// Register adds db info routes for the http request handler, and registers cluster controller callbacks
func (e *ETCD) Register(handler http.Handler) (http.Handler, error) {
e.config.Runtime.ClusterControllerStarts["etcd-node-metadata"] = func(ctx context.Context) {
registerMetadataHandlers(ctx, e)
}
// The apiserver endpoint controller needs to run on a node with a local apiserver,
// in order to successfully seed etcd with the endpoint list. The member removal controller
// also needs to run on a non-etcd node as to avoid disruption if running on the node that
// is being removed from the cluster.
if !e.config.DisableAPIServer {
e.config.Runtime.LeaderElectedClusterControllerStarts[version.Program+"-etcd"] = func(ctx context.Context) {
// ensure client is started, as etcd startup may not have handled this if this is a control-plane-only node
if e.client == nil {
if err := e.startClient(ctx); err != nil {
panic(errors.WithMessage(err, "failed to start etcd client"))
}
}
registerEndpointsHandlers(ctx, e)
registerMemberHandlers(ctx, e)
registerSnapshotHandlers(ctx, e)
// Re-run informer factory startup after core and leader-elected controllers have started.
// Additional caches may need to start for the newly added OnChange/OnRemove callbacks.
if err := start.All(ctx, 5, e.config.Runtime.K3s, e.config.Runtime.Core); err != nil {
panic(errors.WithMessage(err, "failed to start wrangler controllers"))
}
}
}
// Tombstone file checking is unnecessary if we're not running etcd.
if !e.config.DisableETCD {
tombstoneFile := filepath.Join(dbDir(e.config), "tombstone")View on GitHub (pinned to 6ba341e396)