🤔 Why Leader Election?

A controller is stateful — it maintains a cache of the cluster's state (informers) and acts on it. Running two controllers simultaneously without coordination would cause split-brain: both controllers see the same desired state and both try to reconcile, leading to duplicated actions, conflicting updates, and race conditions.

❌ No HA (single replica)

One controller pod. If it crashes, no reconciliation happens until the pod restarts. Recovery time = pod restart time (~30s).

❌ Multi-replica, no election

Multiple controllers all running. Duplicate reconciliations, conflicting updates, resource thrashing. Dangerous.

✅ Multi-replica + leader election

Multiple replicas, only one active leader at a time. Followers are on hot standby. Fast failover on leader crash (~15s).

🔵 All Kubernetes control plane components use this kube-controller-manager, kube-scheduler, and cloud-controller-manager all use lease-based leader election. You can inspect the current leader:
kubectl get lease kube-controller-manager -n kube-system -o yaml
kubectl get lease kube-scheduler -n kube-system -o yaml

Active-passive vs active-active

ModelAll replicasLeader election needed?Examples
Active-passive Only leader reconciles; followers idle on standby Yes — prevents split-brain kube-controller-manager, custom operators
Active-active (sharded) All replicas handle different work (sharded by key) No (or per-shard election) Prometheus, stateless web servers
Active-active (read-only) All replicas serve reads; writes coordinated separately No Metrics aggregators, cache readers

🚀 Production HA Controller Deployment

Running 2–3 replicas with leader election gives fast failover without split-brain. Here's a production-grade Deployment for an operator:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-operator
  namespace: my-operator-system
spec:
  replicas: 2    # 2 replicas: 1 active leader + 1 hot standby
  selector:
    matchLabels: { app: my-operator }
  template:
    metadata:
      labels: { app: my-operator }
    spec:
      serviceAccountName: my-operator
      terminationGracePeriodSeconds: 10
      containers:
      - name: manager
        image: ghcr.io/myorg/my-operator:v1.0.0
        args:
        - "--leader-elect=true"
        - "--health-probe-bind-address=:8081"
        - "--metrics-bind-address=:8080"
        livenessProbe:
          httpGet: { path: /healthz, port: 8081 }
          initialDelaySeconds: 15
          periodSeconds:       20
        readinessProbe:
          httpGet: { path: /readyz, port: 8081 }
          initialDelaySeconds: 5
          periodSeconds:       10
        resources:
          requests: { cpu: 100m, memory: 128Mi }
          limits:   { memory: 256Mi }
        securityContext:
          allowPrivilegeEscalation: false
          runAsNonRoot: true
          seccompProfile: { type: RuntimeDefault }
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              topologyKey: kubernetes.io/hostname
              labelSelector:
                matchLabels: { app: my-operator }

Required RBAC for lease management

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: my-operator-leader-election
  namespace: my-operator-system
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs:     ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["events"]   # for election events
  verbs:     ["create", "patch"]

Graceful leadership handoff

With LeaderElectionReleaseOnCancel: true, when a leader receives SIGTERM (pod shutdown), it releases the lease immediately before exiting — instead of waiting for the lease to expire. This reduces failover time from ~15s to ~2s during planned restarts (rolling updates, node drains):

# Without LeaderElectionReleaseOnCancel:
# 1. Leader receives SIGTERM → starts graceful shutdown
# 2. Standby waits up to LeaseDuration (15s) for lease to expire
# 3. Standby wins election → starts reconciling   (15s gap)

# With LeaderElectionReleaseOnCancel: true:
# 1. Leader receives SIGTERM → releases lease immediately
# 2. Standby sees expired lease → wins election in RetryPeriod (2s)
# 3. Standby starts reconciling                   (2s gap)

Observing leader elections

# Watch the current leader
kubectl get lease my-operator-leader -n my-operator-system -w

# See election history in events
kubectl get events -n my-operator-system \
  --field-selector reason=LeaderElection \
  --sort-by='.lastTimestamp'

# Check which replica is the current leader
kubectl get lease my-operator-leader -n my-operator-system \
  -o jsonpath='{.spec.holderIdentity}'
# my-operator-7d9f8c-xkp2j

# Count leadership transitions (high count = instability)
kubectl get lease my-operator-leader -n my-operator-system \
  -o jsonpath='{.spec.leaseTransitions}'
⚠️ Only use leader election for stateful controllers If your controller only reads state (e.g. a metrics exporter), you don't need leader election — all replicas can run independently. Leader election adds latency (election gap after crash) and API server load (constant lease renewals). Use it only when duplicate reconciliation would cause problems.

🏷️ The Lease Object — How Election Works

Kubernetes leader election uses a Lease object in the coordination.k8s.io/v1 API group. The Lease holds the identity of the current leader and a renewal timestamp. Candidates race to acquire the lease by updating it atomically — the API server's optimistic concurrency (resourceVersion) ensures only one wins.

Replica A LEADER 👑 renews every 2s Replica B (standby) Lease Object holderIdentity: replica-a-pod leaseDurationSeconds: 15 renewTime: 2024-01-15T10:00:02Z acquireTime: 2024-01-15T09:55:00Z leaseTransitions: 2 (stored in etcd) Replica C (standby) polls lease every 2s if expired → tries to acquire renew lease watch lease

Lease object in etcd

# Inspect a controller's current leader
kubectl get lease my-operator-leader -n my-operator-system -o yaml
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  name: my-operator-leader
  namespace: my-operator-system
spec:
  acquireTime:          "2024-01-15T09:55:00.000000Z"
  holderIdentity:       my-operator-7d9f8c-xkp2j  # pod name
  leaseDurationSeconds: 15     # how long the lease is valid
  leaseTransitions:     3      # how many times leadership changed
  renewTime:            "2024-01-15T10:00:04.123456Z"

Enabling leader election in controller-runtime

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme: scheme,

    // Enable leader election
    LeaderElection:                true,
    LeaderElectionID:              "my-operator-leader",        // Lease name
    LeaderElectionNamespace:       "my-operator-system",        // Lease namespace
    LeaderElectionReleaseOnCancel: true,                        // release on graceful shutdown

    // Tuning parameters
    LeaseDuration: func() *time.Duration { d := 15 * time.Second; return &d }(),
    RenewDeadline: func() *time.Duration { d := 10 * time.Second; return &d }(),
    RetryPeriod:   func() *time.Duration { d :=  2 * time.Second; return &d }(),
})

Election parameter tuning

ParameterDefaultMeaningTrade-off
LeaseDuration 15s How long a lease is valid without renewal Longer = slower failover; shorter = more API server load
RenewDeadline 10s How long the leader tries to renew before giving up Must be < LeaseDuration; should be > RetryPeriod × attempts
RetryPeriod 2s How often candidates retry acquiring/renewing Shorter = faster election; more API calls
💡 Failover time = LeaseDuration + RetryPeriod With defaults: if the leader crashes, a standby detects expiry after up to 15s, then wins the election on the next retry (~2s). Total failover ≈ 15–17s. For faster failover, reduce LeaseDuration (but watch API server load).

🧠 Knowledge Check

Q1. Two controller replicas both try to acquire the same Lease simultaneously. What mechanism ensures only one wins?

A) A distributed lock stored in a separate Redis cluster
B> A Kubernetes admission webhook that serialises Lease writes
C) Optimistic concurrency via resourceVersion — the second writer gets a 409 Conflict and must retry
D) A random election algorithm where replicas assign themselves random priorities

Q2. With LeaseDuration: 15s and RetryPeriod: 2s, approximately how long does failover take if the leader crashes?

A) Immediately — the standby detects the crash via the pod watch
B) 5 seconds — the default pod restart backoff
C) ~15–17s — standby waits for lease expiry (15s) then wins on next retry (2s)
D) 30 seconds — the default pod termination grace period

Q3. What does LeaderElectionReleaseOnCancel: true do and why is it important?

A) It prevents the leader from being evicted by node pressure
B) On graceful shutdown the leader releases the lease immediately, reducing failover from ~15s to ~2s
C) It cancels all pending reconcile operations when the leader steps down
D) It automatically cancels the election if only one candidate exists

Q4. You have a metrics-exporter controller that only reads cluster state to build dashboards. Should it use leader election?

A> Yes — all controllers should use leader election for safety
B) No — read-only controllers have no split-brain risk; all replicas can run simultaneously
C) Only if the controller watches more than 5 resource types
D) Only if it runs in the kube-system namespace