Hitesh Sahu
Hitesh SahuHitesh Sahu
  1. Home
  2. ›
  3. posts
  4. ›
  5. …

  6. ›
  7. 2 4 Controllers

Loading ⏳
Fetching content, this won’t take long…


💡 Did you know?

🦈 Sharks existed before trees 🌳.

🍪 This website uses cookies

No personal data is stored on our servers however third party tools Google Analytics cookies to measure traffic and improve your website experience. Learn more

Loading ⏳
Fetching content, this won’t take long…


💡 Did you know?

🦈 Sharks existed before trees 🌳.
kubernetes

    AI-AgenticAI

    AI-DeepLearning

    AI-GenAI

    AI-Infrastructure

    AI-Machine-Learning

    AI-Math

    AWS

    Azure

    Hobbies

    kubernetes
    • Kubernetes and Cloud Native Certification Path

    • Kubernetes: Control Loops, Scheduling, and GPUs

    • Kubernetes API Server Internals

    • etcd Architecture Explained

    • Kubernetes Scheduler Internals

    • Kubernetes Informers & Controllers Explained

    • Kubernetes Networking: Pods, Services, Ingress, and CNI

    • Kubernetes Storage: PV, PVC, StorageClass, and CSI

    • Helm: Kubernetes Package Manager

    • Cloud Native Observability: Prometheus, Grafana, OpenTelemetry, and Tracing

    • GPU Scheduling in Kubernetes: Device Plugins, GPU Operator & MIG

    • NVIDIA Network Operator: InfiniBand, SR-IOV, RDMA, and Multus

    • Dynamic Resource Allocation: The Future of GPU Scheduling in Kubernetes

    • Kubernetes Performance at Scale

    • Optimizing AI Inference at Scale: The Full Stack

    • Kueue: Kubernetes-Native Job Queuing and Quota Management

    • Multi-Node Distributed Training on Kubernetes

    • Kubernetes Topology Manager: NUMA-Aware GPU Scheduling

    • NVIDIA NIM: Optimized Inference Microservices on Kubernetes

    • GPU Autoscaling on Kubernetes: KEDA, HPA, and Cluster Autoscaler

    • kubernetes Index


    Management

    Programming

    Terraform

    Z_Appendix

    0-root

Cover Image for Kubernetes Informers & Controllers Explained
kubernetes

Kubernetes Informers & Controllers Explained

How Kubernetes controllers and informers work — reconciliation loops, shared informers, local caches, work queues with exponential backoff, owner references, generation tracking, finalizers, and the operator pattern.

Kubernetes
Controllers
Informers
Reconciliation
Control Plane
DevOps
← Previous

Kubernetes Scheduler Internals

Next →

Kubernetes Networking: Pods, Services, Ingress, and CNI

Kubernetes Informers & Controllers Explained

One of Kubernetes' greatest strengths is that it is self-healing.

Pods crash. Nodes disappear. Deployments scale. Yet the cluster automatically returns to the desired state.

How?

Through Controllers — control loops that continuously watch the cluster and drive it toward what you declared.

flowchart LR

User["kubectl apply"]

-->APIServer["API Server"]

APIServer

-->Informer

Informer

-->Controller

Controller

-->APIServer

This loop is called the Reconciliation Loop. It is the heart of Kubernetes.


Desired State vs Current State

Kubernetes is a state machine, not a task runner.

You don't tell Kubernetes what to do. You tell it what you want.

replicas: 3

Kubernetes figures out how to get there and keeps it there forever.

Traditional automation Kubernetes controller
Model Imperative — "run this command" Declarative — "make it so"
After failure Stops, requires human Automatically retries
After drift Does nothing Detects and corrects
Lifecycle Runs once Runs forever

The Reconciliation Loop

Every controller runs the same cycle indefinitely.

flowchart LR

Observe["Observe\ncurrent state"]

-->Compare["Compare to\ndesired state"]

Compare

-->Act["Take corrective\naction (if needed)"]

Act

-->Observe

Observe → Compare → Act

The key insight: controllers never "finish". A controller that ran successfully a moment ago runs again when the next event arrives — forever.

This means reconciliation is idempotent. Running it ten times in a row has the same result as running it once. This property is what makes the system resilient: any component can restart at any time and simply pick up where it left off.

The Worker Loop

Inside every controller, a worker goroutine processes items from the work queue:

flowchart LR

Queue

-->Worker

Worker

-->ReadCache["Read desired state\nfrom local cache"]

ReadCache

-->ReadActual["Read current state\nfrom API / cache"]

ReadActual

-->Diff["Diff"]

Diff

-->|"Gap exists"| Act["Call API to\ncorrect gap"]

Diff

-->|"In sync"| Done["Done —\npop from queue"]

Act

-->Done

Reading desired state from the local cache (not the API Server) is critical for performance — it avoids an API round-trip on every reconciliation.


Built-in Controllers

Every Kubernetes resource type has a controller responsible for it. They all live inside a single binary: kube-controller-manager.

Resource Controller What it reconciles
Deployment Deployment Controller Creates/updates ReplicaSets to match spec
ReplicaSet ReplicaSet Controller Creates/deletes Pods to match replicas
StatefulSet StatefulSet Controller Ordered Pod creation and persistent volume binding
DaemonSet DaemonSet Controller Ensures one Pod per matching node
Job Job Controller Tracks completions, retries failures
Node Node Controller Marks nodes unschedulable and evicts pods on failure
Endpoints Endpoints Controller Keeps Service endpoint slices updated
Namespace Namespace Controller Cleans up all resources when namespace is deleted

kube-controller-manager and Leader Election

In an HA control plane, multiple kube-controller-manager replicas run — but only one is active at a time.

They use a leader election lease stored in a ConfigMap or Lease object in etcd. Only the leader runs reconciliation loops. If the leader crashes, a standby acquires the lease within seconds and takes over.

flowchart LR

KCM1["kube-controller-manager\n(leader — active)"]

KCM2["kube-controller-manager\n(standby — watches lease)"]

KCM3["kube-controller-manager\n(standby — watches lease)"]

KCM1

-->Lease["Lease object\nrenewed every 2s"]

KCM1

-->Reconciliation["Running\nreconciliation loops"]

This is the same leader election pattern available to custom controllers via the leaderelection package.


Informers

A controller needs to know when something changes. But polling the API Server continuously would destroy it at scale.

The solution is Informers — a client-side abstraction over the API Server's List+Watch mechanism.

flowchart LR

APIServer["API Server"]

-->|"Watch stream"| Informer

Informer

-->Cache["Thread-safe\nlocal cache"]

Informer

-->EventHandlers["Event handlers\n(OnAdd / OnUpdate / OnDelete)"]

EventHandlers

-->WorkQueue

List + Watch

An Informer starts by listing all current objects, then opens a long-lived watch stream for future changes.

  1. LIST /api/v1/pods — fetch all pods, populate the local cache
  2. WATCH /api/v1/pods?resourceVersion=12345 — receive events from that revision onward

If the watch stream drops (network blip, API Server restart), the Informer reconnects automatically and resumes from its last-seen resourceVersion. No events are lost.

Local Cache (Thread-Safe Store)

After the initial list, the Informer maintains an in-memory store that stays current via watch events.

Controllers always read from this local cache — never directly from the API Server:

// Fast — reads from in-memory cache
pod, err := podLister.Pods("default").Get("my-pod")

// Slow — network round-trip to API Server every time
pod, err := clientset.CoreV1().Pods("default").Get(ctx, "my-pod", metav1.GetOptions{})

The cache is thread-safe. Multiple goroutines can read it simultaneously. This is what makes high-throughput controllers possible — thousands of reconciliations per second without any API Server pressure.

Event Handlers and the Work Queue

Controllers register callbacks on the Informer:

informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
    AddFunc:    func(obj interface{}) { enqueue(obj) },
    UpdateFunc: func(old, new interface{}) { enqueue(new) },
    DeleteFunc: func(obj interface{}) { enqueue(obj) },
})

Critically: handlers enqueue the object key, not the object itself. The actual reconciliation happens later in a worker goroutine — not inside the event handler. This keeps the handler fast and non-blocking.


Shared Informers

Suppose the Deployment Controller and HPA Controller both need to watch Pods.

Without sharing:

flowchart LR

API

-->DeploymentCtrl["Deployment Controller\n(own watch + cache)"]

API

-->HPACtrl["HPA Controller\n(own watch + cache)"]

API

-->RSCtrl["ReplicaSet Controller\n(own watch + cache)"]

Three watches, three caches, three copies of every Pod in memory.

With a SharedInformerFactory:

flowchart LR

APIServer["API Server"]

-->|"one watch"| SharedInformer["Shared Informer\n(one cache)"]

SharedInformer

-->DeploymentCtrl

SharedInformer

-->HPACtrl

SharedInformer

-->RSCtrl

One watch stream. One cache. All controllers share the same data.

In kube-controller-manager, every built-in controller is initialized with the same SharedInformerFactory. This is one of the most important scalability decisions in the Kubernetes codebase.


Work Queue — Reliability Guarantees

The work queue is not a simple FIFO. It provides four guarantees that make controllers robust:

Guarantee What it means
Deduplication If the same key is enqueued 100 times before a worker processes it, it is processed once
Rate limiting Workers are throttled to avoid overwhelming the API Server
Exponential backoff On failure, the key is re-enqueued after 5 ms → 10 ms → 20 ms → ... up to 1000 s
Concurrency Multiple workers drain the queue in parallel (configurable)

Exponential Backoff in Practice

flowchart TD

Reconcile["Reconcile pod/nginx"]

-->|"API Server error"| Fail["Failure"]

Fail

-->Backoff1["Re-enqueue after 5 ms"]

Backoff1

-->|"Still failing"| Backoff2["Re-enqueue after 10 ms"]

Backoff2

-->|"Still failing"| Backoff3["Re-enqueue after 20 ms"]

Backoff3

-->|"Eventually"| Backoff4["Re-enqueue after ~16 min (max)"]

This means a controller that encounters a transient error (API Server blip, etcd timeout) will automatically retry with increasing delays — no manual intervention required.


Owner References and Garbage Collection

Kubernetes objects form a parent-child ownership tree tracked via ownerReferences.

# Pod owned by a ReplicaSet
metadata:
  ownerReferences:
  - apiVersion: apps/v1
    kind: ReplicaSet
    name: web-d5b9f7c44
    uid: a1b2c3d4-...
    controller: true
    blockOwnerDeletion: true

The ownership chain for a Deployment looks like:

flowchart LR

Deployment

-->|"owns"| ReplicaSet

ReplicaSet

-->|"owns"| Pod1

ReplicaSet

-->|"owns"| Pod2

ReplicaSet

-->|"owns"| Pod3

When you delete a Deployment, the Garbage Collector controller follows owner references and deletes all owned objects — ReplicaSets and their Pods.

Two deletion modes:

Mode Behavior
Foreground Parent waits for all children to be deleted before disappearing
Background (default) Parent deleted immediately, children cleaned up asynchronously

Generation and ObservedGeneration

How does a controller know whether it has processed the current spec, or is still acting on a stale one?

Every Kubernetes object has a generation counter in its spec. Every time the spec changes, generation increments. The controller writes observedGeneration into the status once it has fully reconciled that spec version.

# After kubectl set image deployment/web ...
status:
  observedGeneration: 4   # controller has reconciled spec v4
  replicas: 3
  updatedReplicas: 3
  readyReplicas: 3

# If a controller crashes mid-rollout:
status:
  observedGeneration: 3   # controller only processed spec v3
  updatedReplicas: 1      # rollout is incomplete

This lets kubectl rollout status and other tooling know whether a rollout is truly complete or still in progress — without guessing.


Finalizers

When Kubernetes deletes an object, it normally removes it from etcd immediately.

But some resources require cleanup before deletion — releasing a cloud load balancer, removing DNS records, draining a queue.

Finalizers block deletion until a controller performs the cleanup.

flowchart TD

Delete["kubectl delete service lb-svc"]

-->SetTimestamp["API Server sets\ndeletionTimestamp\n(object still exists)"]

SetTimestamp

-->Controller["Controller notices\ndeletionTimestamp"]

Controller

-->Cleanup["Deregisters cloud\nload balancer"]

Cleanup

-->RemoveFinalizer["Controller removes\nfinalizer from object"]

RemoveFinalizer

-->APIServer2["API Server deletes\nobject from etcd"]

If a controller is deleted before removing its finalizer, the object gets stuck with a deletionTimestamp but never disappears. This is the source of the classic "namespace stuck in Terminating" problem.


Reconciliation in Action

A complete walk-through of kubectl apply -f deployment.yaml:

sequenceDiagram

participant User
participant API as API Server
participant DC as Deployment Controller
participant RC as ReplicaSet Controller
participant Sched as Scheduler
participant K as Kubelet

User->>API: Apply Deployment (replicas=3)
API->>DC: ADDED event (via Shared Informer)
DC->>API: Create ReplicaSet (replicas=3)
API->>RC: ADDED event
RC->>API: Create Pod A, Pod B, Pod C
API->>Sched: ADDED events (3 unscheduled Pods)
Sched->>API: Bind Pod A → Node 1
Sched->>API: Bind Pod B → Node 2
Sched->>API: Bind Pod C → Node 3
API->>K: Pod A assigned (Node 1 kubelet)
K->>K: Pull image, start container
K->>API: Update Pod A status → Running

Every step is a reconciliation triggered by an event from the Shared Informer. No component polls. No component directly calls the next one. They are fully decoupled through the API Server.


The Operator Pattern

The same Informer + WorkQueue + Reconcile loop that powers built-in controllers is available to anyone building a custom controller.

This is the Operator pattern — a custom controller that manages a domain-specific resource.

flowchart LR

CRD["Custom Resource\nMyDatabase"]

-->Informer

Informer

-->OperatorController["Operator Controller"]

OperatorController

-->|"Creates"| StatefulSet & Service & ConfigMap

OperatorController

-->|"Reconciles"| MyDatabase["MyDatabase\nstatus"]

Popular examples: Cert-Manager, Strimzi (Kafka), CloudNativePG, Argo Workflows, Kubeflow Training Operator.

The operator extends Kubernetes' self-healing model to your own application lifecycle — backups, failover, schema migrations, scaling — all expressed as a reconciliation loop.


Key Takeaways

Component Responsibility
Controller Runs reconciliation: observes current state, compares to desired, takes corrective action
Informer Wraps List+Watch — populates local cache and fires event handlers
Shared Informer One watch + one cache shared across all controllers — essential for scalability
Local Cache In-memory thread-safe store — controllers read from here, never from the API Server directly
Work Queue Deduplication + rate limiting + exponential backoff + concurrent workers
Owner References Tracks parent-child ownership; drives garbage collection on deletion
Generation / ObservedGeneration Lets a controller signal "I have fully processed spec version N"
Finalizers Block object deletion until a controller completes cleanup
Operator Pattern Extends the same loop to custom resources — the foundation of the cloud-native ecosystem

The controller pattern is why Kubernetes heals itself. No orchestration script, no runbook, no human in the loop — just a loop that runs forever, comparing what is to what should be, and closing the gap.

Hitesh Sahu
Written by Hitesh Sahu, a passionate developer and blogger.

Tue Jul 07 2026

Share This on

← Previous

Kubernetes Scheduler Internals

Next →

Kubernetes Networking: Pods, Services, Ingress, and CNI

kubernetes/2-4-Controllers
Let's work together
+49 176-2019-2523
hiteshkrsahu@gmail.com
WhatsApp
Skype
Munich 🥨, Germany 🇩🇪, EU
Playstore
Hitesh Sahu's apps on Google Play Store
Need Help?
Let's Connect
Navigation
  Home/About
  Skills
  Work/Projects
  Lab/Experiments
  Contribution
  Awards
  Art/Sketches
  Thoughts
  Contact
Links
  Sitemap
  Legal Notice
  Privacy Policy

Made with

NextJS logo

NextJS by

hitesh Sahu

| © 2026 All rights reserved.