Reconciliation Loops (Controller Pattern) in Kubernetes

Reconciliation Loops (Controller Pattern) in Kubernetes

How Kubernetes quietly watches your cluster, notices when reality drifts away from what you asked for, and keeps pulling it back. A complete, step-by-step guide to the one idea that powers almost everything in Kubernetes.

Imagine you tell a friend, “Please keep the living room at 22 degrees.” You do not tell them to press the heater button at 6 PM, then press it again at 8 PM, then open a window at midnight. You just describe the result you want, and your friend keeps checking the room and fixing things until it feels right. If someone opens a window and the room gets cold, your friend notices and turns the heater up again without being asked.

Kubernetes works in exactly this way. You do not tell it, “start three copies of my application, one after another.” You tell it, “I want three copies of my application running at all times.” Then a swarm of small programs called controllers keep checking the cluster, comparing what exists with what you asked for, and fixing every difference they find. The never-ending cycle of check, compare, and fix is called a reconciliation loop.

In this guide you will learn what a reconciliation loop is, why Kubernetes was built around it, how a single controller is constructed inside, what happens step by step when you change something, and how to avoid the mistakes that trip up even experienced engineers. Every new word is explained the first time it appears, so you can read from top to bottom without any earlier knowledge of Kubernetes internals.

1The Big Idea: What Is a Reconciliation Loop?

One simple habit, repeated forever: look at the world, compare it with the goal, and take a small step toward the goal.

A reconciliation loop is a small piece of software that runs the same three steps over and over. First it observes what is really happening. Second it compares that with what is supposed to be happening. Third it acts to close the gap. Then it goes back to the first step and does it all again. The word “reconcile” simply means “make two things agree,” just like reconciling a bank statement with your own notebook of spending.

The program that runs this loop is called a controller. The design idea behind it is called the controller pattern. Kubernetes has a controller for almost every kind of object you can create. There is one for Deployments, one for Jobs, one for Nodes, one for Services, and many more. Each controller is responsible for one small area, and each one follows the same habit.

Everyday analogy

Think of a thermostat on your wall. You set it to 22 degrees. That number is the goal. The thermostat keeps measuring the real temperature. If the room is too cold it switches the heater on, and if the room is warm enough it switches the heater off. It never asks, “what happened five minutes ago?” It only asks, “what is the temperature right now, and what do I want?” A Kubernetes controller thinks the same way.

3 steps
Observe, compare, act
Dozens
of built-in controllers in every cluster
Forever
The loop never finishes by design

Where this idea comes from

The idea is much older than Kubernetes. Engineers who build machines, such as factory robots, aircraft autopilots, and the cruise control in a car, have used feedback loops for a very long time. Cruise control does not press the gas pedal a fixed number of times. It keeps measuring the speed and adjusts the engine until the speed matches the number you chose.

Kubernetes inherited the idea from the systems that came before it inside Google, especially the cluster manager called Borg. Borg kept huge fleets of machines running by constantly comparing what jobs should exist with what actually existed. When Google designed Kubernetes as an open-source cousin of Borg, the loop became the heart of the whole design.

i
Remember this

A controller does not remember a list of commands. It only needs two things: the goal and a way to look at reality. That is why Kubernetes can recover from crashes, restarts, and surprises so gracefully.

2Why Kubernetes Needs Controllers: Declarative Versus Imperative

There are two ways to give instructions to a computer system, and the choice between them explains the whole design.

The first way is the imperative style. You give a list of commands, and the system runs them in order. “Start server A. Start server B. Start server C.” The second way is the declarative style. You describe the final result you want, and the system works out the commands by itself. “I want three servers.”

Imperative instructions look simple, but they break easily in real life. Suppose you order three servers and one of them crashes ten minutes later. Your original commands are already finished. Nobody is watching, so nobody replaces the crashed server. You have to notice the problem yourself, decide what to do, and type new commands. At the scale of thousands of machines, this is impossible to do by hand.

Everyday analogy

Ordering food shows the difference. The imperative way is giving a cook a recipe: “chop onions, heat oil, fry for four minutes.” If the oil is too hot, the cook following the recipe blindly burns the onions. The declarative way is telling a chef, “I want a golden, soft onion dish.” A good chef tastes, adjusts the heat, and keeps working until the dish matches the description. Kubernetes is the chef.

QuestionImperative styleDeclarative style
What do you provide?A list of stepsA description of the final result
Who decides the steps?You doThe system does
What if something breaks later?Nothing happens until a human noticesThe system notices and repairs it
Can you run it twice safely?Often not, you may get duplicatesYes, the second run changes nothing
How does it scale to thousands of objects?Poorly, humans become the bottleneckWell, machines do the checking

The declarative style needs a worker that never sleeps and never forgets the goal. That worker is the controller. Without controllers, a declaration would be a wish written on paper. With controllers, a declaration becomes a promise that the system keeps working to fulfil.

This is also why people say Kubernetes is a “self-healing” system. Self-healing is not magic. It is simply the loop noticing a difference between what you asked for and what exists, and then fixing it. A crashed container, a lost machine, and an accidental deletion are all just differences waiting to be noticed.

3Core Vocabulary: Desired State, Observed State, Spec, Status, and Drift

Six short words that you will see in every Kubernetes conversation about controllers.

Goal

Desired state

What you want the world to look like. For example, “five copies of the web server, using version 2.1.”

Reality

Observed state

What is truly happening right now. For example, “four copies are running, one is starting.”

Field

Spec

Short for specification. The part of an object where you write the desired state. Humans and tools write the spec.

Field

Status

The part of an object where controllers write what they have observed. Controllers write the status, humans only read it.

Gap

Drift

The difference between desired and observed state. Drift is what a reconciliation loop exists to remove.

Result

Convergence

The moment when observed state matches desired state, and the controller has no more work to do for now.

Objects, spec, and status

Everything you create in Kubernetes is stored as an object. A Deployment is an object. A Pod is an object. A Service is an object. Each object has some identity information, such as a name and a namespace, a spec section, and a status section. Think of an object as a form. You fill in the “what I want” box, and the controller fills in the “what I see” box.

Everyday analogy

A hospital chart has two parts. The doctor writes the treatment plan, which is the spec. The nurse writes down the patient’s temperature and pulse every hour, which is the status. The nurse never rewrites the plan, and the doctor does not invent measurements. Keeping the two sides separate is what makes the chart trustworthy.

Generation and observed generation

Every time you change the spec, Kubernetes increases a counter on the object called metadata.generation. When a controller finishes reacting to a change, it writes the number it handled into status.observedGeneration. If the two numbers differ, the controller has not caught up yet. This small trick lets tools such as kubectl rollout status say honestly, “I am still waiting for the controller to notice your change.”

Conditions

Controllers also report health in a list called conditions. A condition has a type such as Available or Progressing, a status of True, False, or Unknown, a short reason, a human-readable message, and the time it last changed. Conditions are the polite way for a controller to say, “here is how I am feeling about this object,” and they are what monitoring tools and humans read first when something looks wrong.

i
Good to know

The split between spec and status is so important that Kubernetes gives status its own separate update path called the status subresource. This means a person editing the spec and a controller writing the status cannot accidentally overwrite each other.

4Architecture: Where Controllers Live in a Cluster

Controllers do not talk to each other directly. They all talk to one shared notice board.

To see where controllers fit, we need to meet the main parts of a Kubernetes cluster. The cluster has a control plane, which is the brain, and many worker nodes, which are the machines that run your applications. The control plane holds the shared notice board and the controllers. The nodes hold the real containers.

Front door

kube-apiserver

The only component that everyone talks to. It checks who you are, validates your request, and stores or fetches objects. Nothing else reads or writes the database directly.

Memory

etcd

The consistent key-value database that stores every object. It is the single source of truth for the whole cluster.

Brain

kube-controller-manager

One program that runs dozens of built-in controllers together, so you do not have to start each one separately.

Matchmaker

kube-scheduler

Watches for Pods with no home and picks a suitable node for each. It follows the same watch-and-act habit as a controller.

Node agent

kubelet

Runs on every node. It watches for Pods assigned to its node and keeps the real containers matching them. It is a reconciliation loop for one machine.

Extension

Custom controllers

Programs that you or vendors add for your own object types, often called operators.

flowchart TB
  U["User or CI pipeline"] -->|"kubectl apply"| API["kube-apiserver"]
  API  ETCD[("etcd datastore")]
  CM["kube-controller-manager running many controllers"] -->|"watch and update"| API
  SCH["kube-scheduler"] -->|"watch and bind pods"| API
  KL["kubelet on each worker node"] -->|"watch pods and report status"| API
  KL --> CR["Container runtime"]
  OP["Custom operator controllers"] -->|"watch and update"| API
    
Figure 1. Every actor talks only to the API server, never directly to each other.

Look carefully at Figure 1. The user sends a request to the API server. The API server saves it in etcd. The controller manager, the scheduler, the kubelets, and any custom operators are all watching the API server for changes. None of them call each other. They coordinate by reading and writing objects on the shared notice board.

This arrangement is called choreography, as opposed to orchestration. In orchestration, one boss tells everyone what to do. In choreography, each dancer listens to the music and reacts to what the others are doing. Choreography has a big advantage: if one dancer falls over, the others do not freeze. They keep dancing, and the fallen dancer rejoins when ready.

Everyday analogy

A busy restaurant kitchen uses an order rail. Waiters clip tickets on it. Cooks look at the rail and pick tickets that match their station. The grill cook does not phone the salad cook. If the salad cook goes on a break, tickets simply wait on the rail until someone is ready. The API server is the order rail.

i
Why this matters

Because controllers are independent, you can restart the controller manager, upgrade a custom operator, or lose a node, and the notice board still holds the full truth. When a controller returns, it simply reads the board and continues.

5Inside One Controller: Informer, Cache, Work Queue, and Workers

A controller is built from a handful of small, well-defined parts. Once you know them, every controller looks familiar.

If every controller asked the API server “what is the state of everything?” many times each second, the API server would collapse. So controllers use a smarter design, found in the Kubernetes client libraries, that keeps the traffic low. Let us walk through each part in the order the data flows.

flowchart LR
  A["API server"] -->|"list once then watch"| I["Informer"]
  I --> C["Local cache with indexes"]
  I -->|"event handlers add keys"| Q["Work queue of object keys"]
  Q --> W["Worker threads"]
  W -->|"read current state"| C
  W -->|"create, update, delete"| A
  W -->|"on failure requeue with backoff"| Q
    
Figure 2. The parts of a typical controller and how data moves between them.
1

The informer lists and then watches

When the controller starts, its informer asks the API server for a full list of the objects it cares about. After that it opens a long-lived watch, which is like subscribing to a news feed. The API server pushes a message every time an object is added, changed, or deleted.

2

The local cache keeps a copy

The informer stores every object it sees in a memory cache on the controller itself. Later, when the controller wants to read an object, it reads from this cache instead of calling the API server. This one trick removes most of the read traffic from the cluster.

3

Event handlers drop a key in the queue

Each time something changes, a small handler writes the object’s key into a work queue. The key is just the namespace and name, such as “shop/checkout”. Notice that the event itself is not stored. Only the name of the object is.

4

The work queue removes duplicates

If the same key is added five times while it is still waiting, the queue keeps only one copy. It also guarantees that two workers never process the same key at the same moment. This keeps work orderly and safe.

5

Workers pull keys and reconcile

A small pool of worker threads takes keys off the queue. For each key, a worker runs the reconcile function: read the latest object from the cache, look at the real world, and do whatever is needed to bring the two together.

6

Success forgets, failure retries

If the reconcile succeeds, the key is forgotten until the next change. If it fails, the key is put back in the queue after a waiting period that grows with each failure, so a broken object does not keep the controller busy.

Everyday analogy

Picture a post office sorting desk. A news ticker (the informer) announces that “parcel for house 12 changed.” A clerk writes “house 12” on a sticky note and puts it on a board (the queue). If three notes say “house 12,” the clerk merges them into one. A delivery worker picks a note, looks at the up-to-date address book (the cache), and then walks out to fix the delivery. The note says only which house to look at, never what to do.

i
Production example

The kube-controller-manager that ships with Kubernetes uses exactly these building blocks for every one of its controllers. Frameworks such as controller-runtime, which sits underneath Kubebuilder and the Operator SDK, wrap the same parts so that people who write custom operators get the cache, queue, and retry behavior for free.

6Level-Triggered Versus Edge-Triggered: The Secret of Reliability

This is the single most important idea in the whole pattern, and it is easy to understand with a doorbell.

There are two ways a system can react to the world. An edge-triggered system reacts to events, meaning the moment something changes. A level-triggered system reacts to the current level, meaning how things are right now. Kubernetes controllers are deliberately level-triggered.

Everyday analogy

A doorbell is edge-triggered. If you are in the shower when it rings, you miss the visitor forever. A glass window in the door is level-triggered. Whenever you walk past, you see whether someone is standing outside. Even if you were busy for an hour, you still see the person who is waiting. The window is much more reliable than the bell.

PropertyEdge-triggeredLevel-triggered
Reacts to“Something just happened”“This is how things are now”
If the controller is down for an hourEvents may be lost, so changes are missedIt reads the current state on return and catches up
If an event arrives twiceWork may be done twiceSecond pass finds nothing to do
If ten changes arrive quicklyTen separate reactionsOne reaction to the latest state
Complexity of the logicMust remember history and orderMust only compare two snapshots

Why does this matter so much? In a large cluster, messages are sometimes delayed, dropped, or delivered twice. Networks glitch. Controllers restart during upgrades. If a controller depended on seeing every single event, one lost message could leave your cluster wrong forever. A level-triggered controller does not care. When it wakes up, it simply looks at everything again and fixes whatever is wrong.

A second benefit is collapsing. Imagine you scale a Deployment from 3 to 4, then to 5, then to 6, all within one second. An edge-triggered controller would process three separate events. A level-triggered controller may only wake up once and see “the goal is 6, I have 3,” and then create three Pods in a single pass. It never needs to know about the intermediate numbers.

!
Common trap

Even though events are used to wake the controller up, the reconcile function must never trust the event’s contents. Events only say “something about this object changed.” The function should always re-read the full current state and decide what to do from scratch.

The safety net called resync

As an extra safety net, informers can periodically replay every object in their cache back to the controller, even if nothing changed. This is called a resync. It does not ask the API server again, it just reminds the controller to take another look. If a bug or a temporary failure caused the controller to miss something, the next resync gives it another chance. Many modern operators also use a “requeue after” feature to wake themselves on a timer for things that Kubernetes cannot announce, such as a certificate that is about to expire.

7Data Flow and Lifecycle: What Happens When You Scale a Deployment

Let us follow one small change all the way from your keyboard to running containers.

Suppose a Deployment named “checkout” currently asks for three Pods, and you change the number to five. This looks like one action, but it triggers a relay race across several controllers. Each runner only does its own small job and then passes the baton by writing a change to the API server.

sequenceDiagram
  participant U as User
  participant API as API Server
  participant DC as Deployment Controller
  participant RC as ReplicaSet Controller
  participant S as Scheduler
  participant K as Kubelet
  U->>API: Change replicas from 3 to 5
  API-->>DC: Watch event for the Deployment
  DC->>API: Update ReplicaSet desired count to 5
  API-->>RC: Watch event for the ReplicaSet
  RC->>API: Create 2 new Pod objects
  API-->>S: Watch event for unscheduled Pods
  S->>API: Bind each Pod to a node
  API-->>K: Watch event for Pods on this node
  K->>API: Report Pod status as Running
  API-->>RC: Watch event for Pod status change
  RC->>API: Update ReplicaSet status
    
Figure 3. A relay race of controllers, with the API server passing the baton each time.
1

You change the goal

You send the new spec to the API server. It checks permission, validates the request, saves it in etcd, and increases the generation counter. At this moment nothing real has changed yet. Only the goal has moved.

2

The Deployment controller notices

Its watch fires, the key lands in its queue, and a worker runs. The worker finds the current ReplicaSet for this version and updates its desired count to five. The Deployment controller does not create Pods. It only manages ReplicaSets.

3

The ReplicaSet controller creates Pod objects

It counts the matching Pods, finds three, wants five, and creates two new Pod objects. These are still only records in the database. No container exists yet.

4

The scheduler assigns a home

The new Pods have no node. The scheduler filters out nodes that lack enough room, scores the rest, and writes the chosen node name into each Pod.

5

The kubelet makes it real

The kubelet on the chosen node sees a Pod assigned to it. It asks the container runtime to pull the image and start the containers, and then reports progress back into the Pod’s status.

6

Status flows back up the chain

The ReplicaSet controller sees the Pods become ready and updates its own status. The Deployment controller sees that and updates the Deployment status. Finally observedGeneration catches up with generation, and tools can say the rollout is complete.

The decision flow inside a single reconcile

Each runner in this relay follows the same small flowchart. It is worth memorizing, because every well-written controller looks like this one.

flowchart TD
  S["Take a key from the queue"] --> G["Read desired state from the cache"]
  G --> E{"Does the object still exist?"}
  E -->|"No"| D["Clean up leftovers and stop"]
  E -->|"Yes"| O["Observe the actual state"]
  O --> X{"Does actual match desired?"}
  X -->|"Yes"| ST["Update status and stop"]
  X -->|"No"| A["Take one step toward desired state"]
  A --> R{"Did the action succeed?"}
  R -->|"Yes"| ST
  R -->|"No"| B["Requeue with backoff"]
  B --> S
    
Figure 4. The decision flow of one reconcile pass.

Notice that the flow never says “remember what happened last time.” Every pass begins fresh. Notice also that the controller takes one step toward the goal and then reports. If more steps are needed, the next pass takes them. This keeps each pass short and keeps the system responsive.

i
Production example

The Horizontal Pod Autoscaler is a controller that runs this flow about every fifteen seconds by default. It reads the current CPU or custom metric, compares it with your target, and calculates a new replica count by scaling the current count in proportion to how far the metric is from the target. It then writes that number into the Deployment’s spec, which starts the relay race above. Companies that handle sudden traffic spikes, such as shopping sites during sales, rely on this chain.

8Built-In Controllers You Already Use Every Day

You have probably been relying on reconciliation loops for years without noticing them.

Workload

Deployment controller

Manages rolling updates by creating a new ReplicaSet and slowly shifting Pods from the old one to the new one while respecting limits on surge and unavailability.

Workload

ReplicaSet controller

Keeps the number of matching Pods equal to the desired count. If a Pod disappears, it makes a replacement.

Workload

StatefulSet controller

Creates Pods with stable names and stable storage, one at a time in order, for databases and other programs that care about identity.

Workload

DaemonSet controller

Makes sure one copy of a Pod runs on every matching node, such as a log shipper or a network agent.

Batch

Job and CronJob controllers

The Job controller runs Pods until the required number have finished successfully. The CronJob controller creates Jobs on a schedule.

Infrastructure

Node controller

Watches node heartbeats. If a node goes silent, it marks the node unhealthy and starts the process of moving its Pods elsewhere.

Networking

EndpointSlice controller

Keeps the list of healthy Pod addresses behind each Service up to date, so traffic never goes to a Pod that is gone.

Housekeeping

Namespace and garbage collector

Clean up everything inside a deleted namespace and remove child objects whose parents no longer exist.

A closer look at the Node controller

The Node controller shows how patient a reconciliation loop can be. Every kubelet sends a small heartbeat to the API server by renewing a tiny object called a Lease. If the Node controller does not see a heartbeat for about forty seconds, which is the default grace period, it marks the node’s Ready condition as Unknown and adds a taint that says the node is unreachable.

The Pods on that node usually carry a built-in toleration that allows them to stay for five minutes after the taint appears. Only after that does the system evict them so that their controllers can create replacements on healthy machines. The delay is deliberate. A short network hiccup should not cause every Pod on a machine to be thrown away. This is a good example of reconciliation including patience, not only speed.

Everyday analogy

A teacher takes attendance. If a student is five minutes late, the teacher does not call the police. The teacher waits, marks the student as “not here yet,” and only starts worrying after a longer time. The Node controller behaves like a calm teacher, not like a panicking one.

Production example: Amazon EKS and Google GKE

Managed services such as Amazon Elastic Kubernetes Service and Google Kubernetes Engine run these control plane controllers for you across several machines and zones, so customers only write their desired state. The customer never starts the Deployment controller or the Node controller, yet both are working for them every second.

9Ownership, Garbage Collection, and Finalizers

Creating things is easy. Cleaning up properly is where the real care is needed.

Owner references: who made this?

When a controller creates a child object, it stamps the child with an owner reference that points back to the parent. A ReplicaSet owns its Pods. A Deployment owns its ReplicaSets. A Job owns its Pods. This tiny label serves two purposes. It tells the controller which objects are really its own, so it does not accidentally touch Pods created by someone else. It also tells Kubernetes what to clean up when the parent is deleted.

Everyday analogy

Owner references are like name tags on lunch boxes in a school fridge. Each box carries a child’s name. At the end of the term, the cleaner throws away every box whose owner has left. Without name tags, the cleaner would either throw away everything or leave smelly boxes forever.

The garbage collector

A built-in controller called the garbage collector watches these owner links. When a parent is deleted, it removes the children that are left behind. There are three styles of deletion. In background deletion, which is the default, the parent disappears first and the children are cleaned up afterward. In foreground deletion, the parent stays visible, marked as being deleted, until all its children are gone. In orphan deletion, the children are left alone and simply lose their owner link.

Finalizers: please wait, I am not done

Sometimes deleting an object in Kubernetes is not enough, because it created something outside the cluster. Imagine a controller that creates a cloud load balancer or a storage disk for each object. If the object vanished instantly, the load balancer would be left behind, silently costing money. A finalizer solves this. It is a text marker in the object’s metadata that says, “do not truly delete me until the controller named here has finished its cleanup.”

When you delete an object that has a finalizer, the API server does not remove it. It only stamps a deletion timestamp on it. The owning controller sees the timestamp, performs the cleanup, and then removes its finalizer. Once the last finalizer is gone, the API server finally deletes the object.

stateDiagram-v2
  [*] --> Active
  Active --> Terminating: User deletes the object
  Terminating --> CleanupRunning: Deletion timestamp set and finalizer present
  CleanupRunning --> Removed: Controller finishes cleanup and removes finalizer
  Removed --> [*]
    
Figure 5. The life of an object that carries a finalizer.
!
Common trap

If a controller with a finalizer is uninstalled or broken, objects with its finalizer can get stuck in the Terminating state forever. Engineers sometimes remove the finalizer by hand to unblock deletion, but doing so skips the cleanup and can leave real resources behind. Always uninstall controllers only after deleting the objects they manage.

Production example: cloud controllers and storage drivers

Persistent volume protection in Kubernetes uses finalizers so that a storage volume cannot be removed while a Pod is still using it. Cloud controller managers use the same idea to make sure that a cloud load balancer behind a Service of type LoadBalancer is deleted before the Service object disappears.

10Writing a Good Reconciler: Idempotency, Conflicts, Retries, and Backoff

The reconcile function looks small, but it has to survive a messy world full of crashes, duplicates, and collisions.

Idempotency: safe to repeat

An action is idempotent if doing it twice has the same result as doing it once. Switching a light to the “on” position is idempotent. Pressing a toggle button is not, because the second press turns it off again. Because Kubernetes may call your reconcile function many times for the same object, it must be idempotent. Instead of “create a Pod,” the logic should be “make sure a Pod exists, and create it only if it does not.”

Everyday analogy

A lift call button is idempotent. Pressing it five times does not bring five lifts. It just says “someone is waiting on this floor.” A good reconciler treats every wake-up call like that button, as a reminder and not as a command to repeat.

Conflicts and optimistic concurrency

Many actors can edit the same object. To prevent silent overwriting, every object carries a resourceVersion, which is like a page edition number. When you update an object, you must say which edition you read. If someone else changed it in the meantime, the API server rejects your update with a conflict error. This approach is called optimistic concurrency, because it assumes conflicts are rare and handles them only when they appear.

The right response to a conflict is not to panic. The reconciler simply stops, returns an error or a requeue request, and tries again. On the next pass it reads the fresh version from the cache and decides again from scratch. Because the logic is level-triggered, nothing is lost.

Retries with exponential backoff

When a reconcile fails, the key goes back into the queue, but not immediately. The queue applies a rate limiter that starts with a very short delay, a few milliseconds, and roughly doubles it after each consecutive failure, up to a maximum of about a thousand seconds. This is called exponential backoff. A broken object slows down gradually and does not overload the controller or the API server. When a pass finally succeeds, the failure count for that key is forgotten.

Everyday analogy

When your friend does not answer the phone, you do not call fifty times in a row. You try again after a minute, then after five minutes, then after an hour. Backoff is the same good manners applied to machines.

Three possible endings of a reconcile

Done

Success, no requeue

The world matches the goal. The key is dropped until a new change arrives.

Later

Success, requeue after a delay

Everything is fine for now, but something may change later, such as a certificate nearing expiry. The controller asks to be woken at a specific time.

Retry

Error, retry with backoff

Something went wrong. The key returns to the queue with a growing delay.

Status updates and the self-trigger loop

A reconciler normally writes the object’s status at the end of every pass. But writing the status changes the object, and that change creates a new watch event, which wakes the same controller again. If the controller is not careful, this can create an endless loop where it keeps waking itself. The fix is to react only to meaningful changes, for example only when the generation number changes, and to avoid writing the status when nothing about it has actually changed.

i
Tip

Compare the new status with the old one before sending an update. An unchanged status needs no write, and every skipped write saves load on the API server and on etcd.

11Custom Controllers and Operators: Teaching Kubernetes New Tricks

The controller pattern is not locked inside Kubernetes. You can use it for anything you can describe as a goal.

Kubernetes lets you invent your own object types using a feature called a Custom Resource Definition, or CRD. A CRD tells the API server, “there is now a new kind of object called PostgresCluster, and here is the shape of its spec.” After that, people can create PostgresCluster objects just like Deployments. But on its own, the object does nothing, because it is only a record.

To bring it to life, you write a custom controller that watches PostgresCluster objects and creates the real Pods, Services, volumes, and backups that match the spec. A custom resource together with its controller is called an operator. The name comes from the idea of packaging the knowledge of a human operator, such as a database administrator, inside software.

Everyday analogy

Think of a CRD as adding a new button to a remote control, and the controller as the electronics that make the button actually do something. Without the electronics, the button is painted plastic.

What operators typically automate

Install

Setup

Creating all the pieces of a complex system from one short description.

Upgrade

Careful upgrades

Rolling out new versions in the right order, with checks between steps.

Heal

Failure recovery

Promoting a replica when a primary database fails, then rebuilding the lost copy.

Protect

Backups and restores

Taking regular snapshots and restoring them on request.

Production example: cert-manager

cert-manager, a widely used project in the Cloud Native Computing Foundation, adds objects like Certificate and Issuer. You declare “I want a valid certificate for this domain,” and its controllers request the certificate from an authority, store it in a Secret, and renew it before it expires. Thousands of companies use it so nobody needs to remember certificate renewal dates.

Production example: Prometheus Operator and Strimzi

The Prometheus Operator turns a short description of a monitoring setup into running Prometheus servers and alert managers. Strimzi does the same for Apache Kafka clusters, handling broker rolling restarts and topic management. Both let platform teams manage complex software with the same declarative habit they use for simple workloads.

Production example: GitOps with Argo CD and Flux

Argo CD and Flux apply the reconciliation idea one level higher. They compare what is written in a Git repository with what is running in the cluster, and they bring the cluster back to match Git whenever someone changes things by hand. Teams at many companies use them so that Git becomes the single, reviewable source of truth.

Production example: Karpenter and Cluster Autoscaler

These controllers watch for Pods that cannot be scheduled because there is no room, and they ask the cloud provider for new machines. When machines sit empty, they remove them to save money. The goal here is not a Kubernetes object but a cloud bill, and the same loop works.

Controllers beyond the cluster

Projects like Crossplane and the AWS Controllers for Kubernetes extend the pattern to cloud resources themselves. You describe a database or a storage bucket as a Kubernetes object, and a controller creates and maintains the real cloud resource. Kubernetes becomes a general-purpose goal-keeping engine, not only a container launcher.

i
Good to know

Most teams write operators with frameworks such as Kubebuilder or the Operator SDK. These handle the informer, cache, queue, leader election, and metrics, so the engineer only writes the reconcile logic that is unique to the business problem.

12High Availability, Leader Election, and Failure Recovery

A controller that runs on a single machine is a single point of failure. Kubernetes solves this without giving up simplicity.

In a production cluster, the control plane usually runs on three or more machines so that no single failure stops it. But if three copies of the same controller all acted at the same time, they would trip over each other, creating duplicate Pods and fighting over the same fields. The answer is leader election. Many copies run, but only one is the active leader. The others stay on standby, watching, ready to take over.

Leader election in Kubernetes is built on a small object called a Lease. The leader keeps renewing the lease, like keeping its hand raised. By default the lease lasts about fifteen seconds, the leader tries to renew it for about ten seconds, and standby copies check every couple of seconds. If the leader crashes and stops renewing, the lease expires, and one standby grabs it and becomes the new leader.

Everyday analogy

Think of a relay baton at a school sports day, or better, a talking stick in a meeting. Only the person holding the stick may speak. If the holder walks out of the room, the stick eventually gets passed to someone else. Everybody else keeps listening, so nothing is lost.

What happens when each part fails

What failsWhat happensHow the system recovers
A worker thread inside a controllerOne reconcile pass is lostThe key returns to the queue and is retried
The whole controller processNo reconciling for a short timeA standby takes over, reads current state, and continues
A watch connectionEvents stop arrivingThe informer reconnects, and if its place was lost, it lists again from scratch
The API serverControllers cannot read or writeThey retry with backoff, and others in the pool serve traffic
A worker nodePods on it stop runningThe Node controller marks it, then Pods are replaced elsewhere
An external cloud APIOperator actions failBackoff and later retry, with status conditions showing the problem

Look at the “recovers” column. The recovery method is nearly always the same: read the current state and carry on. There is no special failure code for every case. The ordinary loop is the recovery plan. This is the quiet superpower of the pattern, and it is why Kubernetes clusters can survive upgrades, crashes, and network blips with so little drama.

!
Common trap

A brief period with two leaders can occur during a network split. Well-written controllers protect against harm by using optimistic concurrency, so even if two copies try to act, the second write is rejected as stale. Never keep important state only in a controller’s memory.

Production example: control plane upgrades

Large operators of Kubernetes, including managed service providers, routinely restart controller manager replicas during upgrades. Customers rarely notice, because the incoming leader simply reads the cluster’s state and continues exactly where the outgoing leader stopped.

13Performance and Scalability

A loop that runs forever must be cheap, or the whole cluster pays the price.

The official Kubernetes guidance for a single cluster says it is designed for up to about five thousand nodes, around one hundred and fifty thousand Pods in total, and no more than one hundred and ten Pods per node. These numbers are possible largely because of the careful design you saw in Chapter 5. Let us look at the techniques that keep the load manageable.

Reads

Local caches

Controllers read from memory, not from the API server. A thousand reads cost nothing on the network.

Network

Watch instead of poll

One long-lived connection replaces thousands of repeated questions.

Queue

Deduplication

Many events for one object become a single piece of work.

Parallel

Worker concurrency

Several workers handle different keys at once. Built-in controllers expose settings for how many, for example five Deployment workers by default.

Fairness

API Priority and Fairness

The API server sorts incoming requests into priority levels so that a noisy client cannot starve important ones.

Rate

Client-side rate limits

Controllers limit how fast they send requests, protecting the API server even when they have a huge backlog.

The thundering herd after a restart

When a controller starts, its first list brings in every object, and every object is placed in the queue once. A controller that manages fifty thousand objects therefore has fifty thousand pieces of work ready at the same moment. This surge is called a thundering herd. If each reconcile makes expensive calls to the API server or to an outside service, the start-up can overload the very systems it depends on. Careful controllers keep reconciles cheap when nothing needs changing, use caches for reads, and apply their own rate limits to external calls.

Everyday analogy

It is like a shop opening its doors after a holiday. If every customer rushes in at once, the single cashier is overwhelmed. A queue at the door that lets people in at a steady speed keeps everyone happy.

Reconcile time matters

Each worker handles one key at a time, so a slow reconcile blocks that worker. If a pass waits ten seconds for an outside service, and there are only a few workers, the queue can pile up. Good reconcilers avoid waiting inside the function. Instead of sleeping until a database becomes ready, they record the current state and ask to be requeued shortly, freeing the worker for other keys.

i
Tip

Keep the objects small. etcd limits a single request to roughly one and a half megabytes by default, and big objects slow down every watcher. Store large data in a proper external store and keep only references in the spec.

Production example: very large clusters

Companies that run clusters with thousands of nodes, such as large e-commerce platforms and streaming services, tune worker counts, rate limits, and the sizes of their objects to keep controller queues short. They watch queue depth the way a restaurant watches the order rail.

14Security: Controllers Are Powerful, So Treat Them Carefully

A controller can create, change, and delete things automatically, so it must hold only the keys it truly needs.

Every controller talks to the API server using an identity, usually a ServiceAccount. The permissions of that identity are controlled by RBAC, which stands for role-based access control. RBAC is a list of rules that says which actions, such as get, list, watch, create, update, or delete, are allowed on which kinds of objects in which namespaces.

Everyday analogy

In a hotel, the cleaner has a key that opens guest rooms for a few hours. The kitchen worker’s key opens the pantry. Nobody has a master key unless their job truly needs it. Giving a controller the cluster-admin role is like handing the kitchen worker the master key to every room and the safe.

Identity

One identity per controller

The built-in controller manager can use a separate ServiceAccount for each controller, so each one has narrow permissions.

Least privilege

Only the verbs needed

If a controller only reads Secrets, do not allow it to delete them. Limit permissions to specific kinds and namespaces.

Validation

Admission checks

Admission controllers and validating webhooks reject bad specs early, before a controller can act on harmful input.

Isolation

Run with limited power

Run controller Pods as non-root with read-only file systems where possible, and keep them on trusted nodes.

Why a compromised controller is dangerous

A controller with broad rights is a high-value target. If an attacker takes control of an operator that can read all Secrets and create Pods anywhere, they can read passwords and run their own programs on your nodes. This is called privilege escalation. Reducing the permissions of every controller limits the damage if one is ever tricked or taken over.

Trust the cluster, but validate the input

Users can write anything into a custom resource spec. A good operator treats the spec as untrusted input. It checks values, rejects unsafe combinations, and never uses spec text to build commands or file paths directly. The CRD itself can carry a schema that makes the API server reject malformed objects before the controller sees them.

!
Common trap

Many public Helm charts for operators request cluster-wide permissions by default. Always read the roles they create and trim them to what your use case needs.

Production example: separation of duties in platform teams

Platform teams at large companies commonly allow application teams to create custom resources such as a database request, while only the operator holds the permission to create the actual cloud database or storage. The developers describe, and the controller holds the dangerous keys, with an audit trail of every action.

15Monitoring, Logging, and Metrics for Controllers

A controller works quietly in the background, so you need instruments to know that it is healthy.

Because a controller has no screen and no user, you learn about its health from the numbers it publishes. Controllers built with the standard libraries expose metrics in a format that Prometheus can collect. The most useful ones tell you how busy the queue is, how long work takes, and how often work fails.

SignalWhat it tells youWhat a bad value looks like
workqueue_depthHow many keys are waitingKeeps growing, so the controller cannot keep up
workqueue_queue_duration_secondsHow long keys wait before being handledLong waits mean slow reaction to changes
workqueue_work_duration_secondsHow long each reconcile takesSlow passes starve other keys
workqueue_retries_totalHow often work is retriedA steady climb points to a persistent problem
controller_runtime_reconcile_errors_totalHow many reconciles failAny sustained rate needs investigation
rest_client_requests_totalHow many calls go to the API serverA sudden spike may signal a hot loop

Logging with context

Good controllers write structured logs that always include the object’s kind, namespace, and name, so you can follow one object’s story from start to finish. They log decisions, such as “creating Pod because only 2 of 3 exist,” not just errors. They avoid logging secrets and large payloads, and they use levels so that noisy detail can be switched on only when you are debugging.

Events and conditions: talking to humans

Kubernetes has a built-in way for controllers to leave short, human-friendly messages called Events. When you run a describe command on an object and see lines like “Scaled up replica set” or “Failed to pull image,” you are reading events. Combined with status conditions, they form the first place to look when something seems stuck. A controller that never writes events or conditions is hard to trust, because it fails silently.

Everyday analogy

A car dashboard does not show you the engine. It shows a fuel gauge, a temperature gauge, and warning lights. Metrics, events, and conditions are the dashboard of a controller. You do not need to open the engine unless a light comes on.

i
Tip

Alert on queue depth and on “time since last successful reconcile” for important objects. A controller can look alive while being stuck, and these two signals catch that situation early.

Production example: dashboards for control plane health

Teams that run Kubernetes seriously build dashboards in tools such as Grafana that show work queue depth and latency for every controller in the control plane, next to API server latency. When a rollout feels slow, these charts tell them whether the delay lives in a controller, in the scheduler, or in the nodes.

16Advantages, Disadvantages, and Trade-offs

Every design has a price. Knowing it helps you decide when the pattern is the right tool.

Advantages

  • Self-healing: drift is repaired without human help.
  • Resilient to failure: a restart only means reading the latest state again.
  • Simple mental model: goal on one side, reality on the other, one loop in between.
  • Loose coupling: controllers do not know about each other, so each can be built, tested, and replaced alone.
  • Extensible: anyone can add new object types and new controllers.
  • Auditable: the goal is stored as data that can be reviewed, versioned, and rolled back.

Disadvantages

  • Eventual consistency: results appear after a delay, not instantly.
  • Hard to debug: a chain of controllers means the cause of a problem may be several steps away.
  • Hidden load: badly written loops can quietly hammer the API server.
  • Fighting controllers: two controllers that disagree about the same field can undo each other forever.
  • Operational weight: every operator is another program to upgrade, secure, and monitor.
  • Not for every task: one-time jobs with strict ordering may fit simple scripts better.
Trade-offChoice Kubernetes makesWhat you gainWhat you give up
Speed versus safetySmall steps, repeatedFewer big mistakesChanges take several passes
Events versus stateLevel-triggeredReliability after missed messagesSlight extra reading each pass
Central versus independentMany independent controllersEasy to extend and isolateNo single place shows the whole story
Freshness versus loadCached readsVery low API trafficCache can be a few moments behind
Retry speed versus stabilityExponential backoffProtects against overloadA fixed problem may take time to be retried

Eventual consistency in plain words

“Eventually consistent” means that if you stop making changes, the system will become correct, but perhaps not at this very second. When you scale a Deployment, you will not see five ready Pods the moment you press Enter. You will see the number rise over a few seconds as each controller does its part. For almost all workloads this delay is harmless, but you should never write scripts that assume instant results. Wait for the status to report success instead.

The loop does not promise speed. It promises that it will never stop trying.

17Design Patterns and Anti-Patterns

A few good habits make controllers reliable. A few bad ones make them dangerous.

Patterns worth copying

Pattern

Observe, diff, act

Always build the plan by comparing two snapshots, never by following a remembered list of steps.

Pattern

One owner per field

Decide which controller owns each field, and let nobody else write it. Server-side apply helps track who owns what.

Pattern

Hierarchy of controllers

A parent controller manages a child object type, and a second controller manages the grandchildren, like Deployment, ReplicaSet, and Pod.

Pattern

Status as the source of progress

Write what you observed into status and conditions, so humans and other tools can see progress without guessing.

Pattern

Finalizer for outside cleanup

Whenever you create something outside the cluster, attach a finalizer to guarantee removal.

Pattern

Small steps

Do one safe change per pass, then re-observe. It limits the damage of any mistake.

Anti-patterns to avoid

ANTI-PATTERN 01Avoid
The mistake

Treating watch events as commands, for example “an Added event arrived, so create one Pod.”

Why it hurts

Events can be duplicated, delayed, or lost. A missed event leaves the cluster wrong forever, and a duplicate creates extra resources.

Do this instead

Use the event only as a wake-up call. Re-read the current state and decide what is missing.

ANTI-PATTERN 02Avoid
The mistake

Keeping important information only in the controller’s memory, such as a counter of how many Pods it created.

Why it hurts

When the controller restarts or the leader changes, the memory is gone, and the new leader makes wrong decisions.

Do this instead

Store durable facts in the object’s status or annotations, and treat memory as a disposable cache.

ANTI-PATTERN 03Avoid
The mistake

Two controllers managing the same field, for example one operator setting the replica count while an autoscaler also sets it.

Why it hurts

Each one reverses the other’s change, causing flapping, wasted work, and confusing behavior.

Do this instead

Give one controller clear ownership. If an autoscaler manages replicas, leave the replica field out of the declared spec.

ANTI-PATTERN 04Avoid
The mistake

Blocking inside reconcile, such as sleeping while waiting for a database to become ready.

Why it hurts

The worker is stuck, and other objects wait in line behind it.

Do this instead

Record the waiting state in status and ask to be requeued after a short time.

ANTI-PATTERN 05Avoid
The mistake

Hot loops: updating something on every pass so that the update triggers the next pass.

Why it hurts

The controller consumes CPU and floods the API server and etcd with useless writes.

Do this instead

Write only when something actually changed, and filter watch events so that harmless updates do not trigger work.

18Best Practices and Common Mistakes

A short checklist to read before you build or install any controller.

1

Make every action idempotent

Check whether the thing already exists before creating it, and compare before updating. Running the same pass ten times should leave the world exactly as one pass would.

2

Always set owner references on children

It keeps cleanup automatic and prevents orphaned objects from piling up and costing money.

3

Report status honestly

Update conditions and the observed generation. Use clear reasons and messages so a tired engineer at midnight can understand what is wrong.

4

Handle deletion on purpose

Use finalizers for external resources, and make the cleanup path as carefully tested as the creation path.

5

Limit permissions

Write the narrowest RBAC rules that still allow the work, and review them whenever the controller gains new features.

6

Run more than one replica with leader election

Standby copies cut recovery time from minutes to seconds, and the upgrade story becomes much smoother.

7

Expose metrics and write useful events

Without them, you will be guessing when something goes wrong.

8

Test the nasty cases

Kill the controller halfway through, deliver the same event twice, delete objects during a pass, and break the outside service. If the loop still converges, you have built it well.

Mistakes beginners make as cluster users

  • Fixing symptoms by hand. If you delete a Pod that a ReplicaSet owns, it will simply come back. Change the owner’s spec instead.
  • Editing child objects. Changes made directly to a Pod owned by a Deployment will be overwritten or lost at the next rollout. Edit the Deployment.
  • Expecting instant results. Wait for the rollout to report success instead of assuming it is done after a second.
  • Ignoring status and events. They are the controller’s explanation of what it is doing. Read them before guessing.
  • Force-removing finalizers. It unblocks the screen but may leave real resources running and billing you.
  • Letting two tools manage the same object. For example, a GitOps tool and a person editing live objects. The tool will keep reverting the person.
i
A calm debugging order

When something is wrong, first read the object’s conditions, then its events, then the controller’s logs, and only then look at the nodes. Following the relay race in Figure 3 from the top usually leads you straight to the runner who dropped the baton.

19Real-World and Industry Examples

The pattern is not a classroom idea. It runs the infrastructure of some of the largest engineering teams in the world.

Google: from Borg to Kubernetes

Google ran its internal workloads on Borg for years before open-sourcing Kubernetes. The core habit, declaring what should run and letting loops keep it running, came directly from that experience. Kubernetes carried that lesson to the whole industry and became a project of the Cloud Native Computing Foundation.

Spotify: moving to a shared platform

Spotify moved many of its backend services onto Kubernetes from an earlier in-house system called Helios. Engineers describe their services and the platform’s controllers handle placement, restarts, and rollouts, which reduced the manual work needed to run thousands of services.

Airbnb, Shopify, and Pinterest: elastic traffic

Companies with sharp daily or seasonal traffic differences use autoscaling controllers, so the number of running Pods rises and falls with demand. The autoscaler writes a new goal, and the Deployment and ReplicaSet controllers carry it out. Engineers do not need to be awake to add capacity.

Banks and large enterprises: policy and drift control

Many financial and enterprise teams use GitOps controllers such as Argo CD and Flux, along with policy engines, so that the cluster is always pulled back to the reviewed, approved configuration. If an engineer changes something by hand during an incident, the loop reports or reverts the drift, which also helps with audits.

Cloud providers: managed control planes

Amazon EKS, Google GKE, and Azure AKS all run the standard controllers for customers, across multiple machines for safety. Customers also install operators on top, such as cert-manager or database operators, getting the same declarative style for every layer of their system.

i
The common thread

In every example, people describe the goal in one place, and software loops do the repetitive, careful, never-tired work of making it real. The human role changes from pressing buttons to writing clear goals.

20Frequently Asked Questions

Short answers to the questions that beginners ask most often.

Q1Is a controller the same as an operator?
Not exactly. A controller is any program running a reconciliation loop. An operator is a controller (or several) that manages a custom resource and carries the know-how of running a specific application, such as a database. All operators are controllers, but not all controllers are operators.
Q2How often does a controller run?
It runs whenever something it watches changes, plus on any timers it has requested, plus on occasional resyncs. There is no fixed clock for most controllers. A quiet cluster means quiet controllers.
Q3What happens if I delete a Pod that a Deployment manages?
The ReplicaSet controller sees that the count dropped below the goal and creates a replacement. To remove the Pod permanently, change the replica count or delete the Deployment.
Q4Can two controllers watch the same object?
Yes, and this is normal. Many controllers may read the same object. Trouble only starts when two of them try to write the same field. Each field should have one clear owner.
Q5Why does my rollout seem slow even though nothing is broken?
A rolling update moves through several controllers, and each step waits for the Pods to become ready. Settings that limit how many Pods can be added or removed at a time make updates safer but slower. Readiness checks that take a long time also add delay.
Q6Does the controller talk to the nodes directly?
No. Controllers talk only to the API server. The kubelet on each node watches the API server and acts on its own machine. The API server is the shared notice board for everyone.
Q7What is the difference between a resync and a re-list?
A resync replays the objects already in the controller’s local cache, without contacting the API server. A re-list is a full fresh read from the API server, which happens when the watch connection breaks in a way that cannot be resumed.
Q8Why are controllers not just a cron job that runs every minute?
A scheduled job would react slowly, waste effort when nothing changed, and put heavy load on the API server by reading everything every time. Watches, caches, and queues react in moments and cost almost nothing when the cluster is calm.
Q9What stops a controller from being overloaded by a broken object?
Backoff. Each failure increases the waiting time before that object is retried, so one broken object cannot consume all the attention of the controller.
Q10Is the reconciliation pattern only for Kubernetes?
No. Thermostats, autopilots, and many cloud management tools use the same feedback idea. Kubernetes simply made it a central, reusable design for managing software infrastructure, and tools like Terraform-based pipelines and GitOps engines apply similar thinking.
Q11Can a controller manage things outside Kubernetes?
Yes. Any outside resource that has an API can be managed by a controller, including cloud databases, DNS records, and storage buckets. Use finalizers so that outside resources are cleaned up when the object is deleted.
Q12How do I know if a controller has finished with my change?
Compare the object’s generation with its observed generation, and check the conditions. When the observed generation equals the generation and the conditions say the object is available, the controller has caught up with your latest goal.

21Summary and Key Takeaways

You started with a thermostat and finished with a deep view of how Kubernetes keeps thousands of moving parts in order. The same small habit explains the whole picture: look at reality, compare it with the goal, take a small step, and repeat. Each controller is simple, but many simple loops working together on a shared notice board create a system that heals, scales, and upgrades itself.

Key Takeaways

  • A reconciliation loop observes, compares, and acts forever, closing the gap between desired state and observed state.
  • Kubernetes is declarative: you write the goal in the spec, and controllers write what they see in the status.
  • Controllers are level-triggered: events only wake them up, and every pass re-reads the current state, so lost or duplicate events do no harm.
  • Every controller shares the same anatomy: an informer, a local cache, a deduplicating work queue, worker threads, and retries with exponential backoff.
  • Controllers coordinate through the API server, not by calling each other, which makes the system loosely coupled and easy to extend with custom resources and operators.
  • Good reconcilers are idempotent, own their children through owner references, clean up with finalizers, and report progress through status, conditions, and events.
  • Run controllers safely: use leader election for availability, least-privilege RBAC for security, and queue and error metrics for early warning.