♻️ The Reconciliation Pattern
Every Kubernetes controller implements the same pattern: Observe → Diff → Act. Controllers never push commands at the cluster; instead they continuously compare desired state (from the API server) with actual state (from the cluster or cloud) and apply the minimum change needed to close the gap.
This makes controllers idempotent and self-healing — re-running the same reconcile loop has no side-effects if the state is already correct.
Watch API server for changes
Desired vs actual state
Create / Update / Delete
Re-queue or watch
kube-controller-manager is a single process running ~40 controllers as goroutines. They share a single API server connection but each maintains its own informer cache and work queue. Only the leader instance is active when running HA.
🎛️ Key Built-in Controllers
| Controller | Watches | Reconciles |
|---|---|---|
| Deployment | Deployment, ReplicaSet | Creates/scales ReplicaSets; manages rolling updates |
| ReplicaSet | ReplicaSet, Pod | Ensures spec.replicas Pods are running; creates/deletes Pods |
| StatefulSet | StatefulSet, Pod, PVC | Ordered pod creation/deletion; stable network IDs; per-pod PVCs |
| DaemonSet | DaemonSet, Node, Pod | Ensures one Pod per matching node; handles node adds/removes |
| Job | Job, Pod | Creates pods to completion; retries on failure up to backoffLimit |
| CronJob | CronJob, Job | Creates Jobs on schedule; enforces concurrencyPolicy; prunes old Jobs |
| Namespace | Namespace | Finalises namespace deletion — removes all resources in the namespace |
| ServiceAccount | Namespace, ServiceAccount | Creates default SA and token in every new namespace |
| Node | Node | Marks nodes NotReady; evicts pods from unreachable nodes after timeout |
| NodeLifecycle | Node, Pod | Adds node.kubernetes.io/not-ready taint; triggers pod eviction |
| PersistentVolume | PV, PVC | Binds PVCs to PVs; handles reclaim policy (Delete/Retain/Recycle) |
| EndpointSlice | Service, Pod | Maintains EndpointSlice objects from ready pod IPs |
| GarbageCollector | All resources | Deletes owner-referenced resources when owners are gone |
| HorizontalPodAutoscaler | HPA, metrics | Adjusts spec.replicas on target based on metric thresholds |
| TokenCleaner | Secret | Removes expired bootstrap tokens |
Deployment Controller — Rolling Update Flow
When you change a Deployment's pod template the Deployment controller executes a rolling update through ReplicaSets:
# Watch what the Deployment controller does during a rollout
kubectl rollout status deployment/nginx --watch
# Under the hood:
# 1. Deployment controller creates a new ReplicaSet (rs-v2) with replicas=0
# 2. It scales rs-v2 up by maxSurge (default 25%) and rs-v1 down by maxUnavailable
# 3. Loops until rs-v2 = desired replicas and rs-v1 = 0
# 4. Old ReplicaSet is retained (for rollback) but scaled to 0
# Check ReplicaSet history
kubectl get replicasets -l app=nginx
# NAME DESIRED CURRENT READY AGE
# nginx-7d8f9c 3 3 3 5m ← new
# nginx-5b6c7d 0 0 0 2h ← old (kept for rollback)
# Rollback uses the old ReplicaSet directly — instant
kubectl rollout undo deployment/nginx
GarbageCollector Controller
The GarbageCollector runs a background graph of owner references. When an owner is deleted, it cascades deletion to all dependents — this is how deleting a Deployment also deletes its ReplicaSets and Pods.
# OwnerReferences — set automatically by controllers
kubectl get pod nginx-7d8f9c-abc -o jsonpath='{.metadata.ownerReferences}'
# [{"apiVersion":"apps/v1","kind":"ReplicaSet","name":"nginx-7d8f9c",
# "uid":"...","controller":true,"blockOwnerDeletion":true}]
# Foreground deletion: owner stays until all dependents are gone
kubectl delete deployment nginx --cascade=foreground
# Background deletion (default): owner deleted first, GC cleans up dependents async
kubectl delete deployment nginx --cascade=background
# Orphan: delete owner but keep dependents
kubectl delete deployment nginx --cascade=orphan
NodeLifecycle Controller — Pod Eviction
When a node goes unreachable, the NodeLifecycle controller adds a node.kubernetes.io/not-ready taint. After the pod eviction timeout (default 5 minutes), it evicts all Pods on that node. This is why pods reappear on other nodes after a node failure.
# Key flags controlling eviction timing
--node-monitor-grace-period=40s # time before node marked NotReady
--pod-eviction-timeout=5m0s # (deprecated) grace period before eviction
# Modern: controlled by node taint tolerations on pods
# Default toleration: 300s for not-ready / unreachable taints
⚙️ Work Queues, Leader Election & Rate Limiting
Work Queue Internals
Each controller uses a rate-limited work queue from client-go/util/workqueue. Events from the informer (watch cache) are enqueued as keys (namespace/name). The reconciler dequeues keys and processes them — deduplication means multiple rapid changes to the same object produce a single reconcile.
Deduplication
If an object is modified 10 times before the reconciler runs, only one reconcile fires — it always reads the latest state from the cache.
Rate Limiting
Failed reconciles are re-queued with exponential back-off (5ms → 1000s cap). Prevents a broken object from hammering the API server.
Parallel Workers
Each controller runs N goroutine workers (default varies). Increasing workers speeds up reconciliation at the cost of more API server load.
Requeue After
Controllers can request re-queue after a fixed duration (e.g. HPA re-queues every 15s to check metrics even with no change events).
// Simplified work queue pattern used by all controllers
func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
defer c.queue.ShutDown()
// Start informers
go c.podInformer.Run(stopCh)
// Wait for cache sync
cache.WaitForCacheSync(stopCh, c.podInformer.HasSynced)
// Start N worker goroutines
for i := 0; i < workers; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
<-stopCh
}
func (c *Controller) runWorker() {
for c.processNextItem() {}
}
func (c *Controller) processNextItem() bool {
key, quit := c.queue.Get()
if quit { return false }
defer c.queue.Done(key)
err := c.reconcile(key.(string))
if err != nil {
// Exponential back-off re-queue
c.queue.AddRateLimited(key)
}
return true
}
Leader Election
In HA control planes, multiple kube-controller-manager instances run simultaneously. Only the leader runs the reconcile loops — the others are hot-standby. Leadership is held via a Lease object in the kube-system namespace, renewed every few seconds.
# View the current leader lease
kubectl get lease kube-controller-manager -n kube-system -o yaml
# spec:
# holderIdentity: master-1_abc-uuid ← current leader
# leaseDurationSeconds: 15
# renewTime: "2024-01-15T12:34:56Z"
# acquireTime: "2024-01-15T10:00:00Z"
# If the leader fails to renew within leaseDurationSeconds,
# a standby instance acquires the lease and becomes leader
# (typical failover: 15–30 seconds)
Key kube-controller-manager Flags
# Concurrency / workers per controller
--concurrent-deployment-syncs=5 # default 5 deployment workers
--concurrent-replicaset-syncs=5
--concurrent-endpoint-syncs=5
--concurrent-gc-syncs=20 # GarbageCollector workers
# Node eviction / health
--node-monitor-grace-period=40s # wait before marking node NotReady
--node-monitor-period=5s # how often to check node status
# Leader election
--leader-elect=true
--leader-elect-lease-duration=15s
--leader-elect-renew-deadline=10s
--leader-elect-retry-period=2s
# HPA
--horizontal-pod-autoscaler-sync-period=15s
--horizontal-pod-autoscaler-tolerance=0.1
# Namespace lifecycle
--namespace-sync-period=5m0s
📊 Observability & Production Tips
Key Metrics
| Metric | Alert threshold | What it means |
|---|---|---|
workqueue_depth{name="deployment"} | > 100 sustained | Controller falling behind — too many changes or too few workers |
workqueue_retries_total | High rate | Reconcile errors causing repeated re-queues |
workqueue_queue_duration_seconds p99 | > 30s | Items waiting too long to be processed |
rest_client_requests_total{code="429"} | > 0 | API server rate-limiting the controller — reduce workers or tune QPS |
node_collector_unhealthy_nodes_in_zone | > 0 | Nodes failing health checks — may trigger evictions |
Common Issues & Fixes
# Issue: Deployment stuck in rollout — pods not becoming ready
kubectl rollout status deployment/myapp
# Waiting for deployment "myapp" rollout to finish: 1 out of 3 new replicas updated
# Diagnose: check events and pod status
kubectl describe deployment myapp
kubectl get pods -l app=myapp --sort-by='.status.startTime'
kubectl describe pod myapp-<new-rs>-<hash>
# Issue: Pods not evicted after node failure (5min default delay)
# Speed up for testing — reduce toleration on pods
spec:
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 30 # evict after 30s instead of 300s
# Issue: HPA not scaling
kubectl describe hpa myapp-hpa
# Check: is metrics-server running?
kubectl top pods -n my-namespace
# Check: are resource requests set on pods? (HPA requires them)
Increase Workers for Large Clusters
On clusters with thousands of Deployments, increase --concurrent-deployment-syncs to reduce reconcile latency during rollouts.
Monitor Work Queue Depth
Persistent work queue depth means the controller is overwhelmed. Tune workers, reduce churn, or check for reconcile errors causing retries.
HA Control Plane
Run 3 controller-manager replicas with leader election. Failover takes ~15s when a leader dies — plan accordingly for critical operations.
Watch Cache Warm-Up
On startup, controllers wait for the informer cache to sync before processing. Large clusters with many objects have a longer warm-up — don't restart unnecessarily.
📝 Knowledge Check
ownerReferences point to a non-existent owner. It then issues delete requests for those dependents (ReplicaSets → Pods). This is cascade deletion via owner references.NotReady after --node-monitor-grace-period (40s default), adding a not-ready taint. Pods tolerate this taint for tolerationSeconds (300s by default). After that, the taint-based eviction controller deletes the pods so they can be rescheduled elsewhere.