What is a shared informer in the Kubernetes client libraries, and what are the consequences of reading objects from its local cache instead of from the API server?
answer
- Reflector -> DeltaFIFO -> Indexer -> Lister
- shared = one watch + one cache per type per process
- cache is eventually consistent; no read-your-own-write
- deep-copy before mutating shared objects
- resync replays the cache, it does not refetch
basics
~20 sAn informer runs one list-watch per resource type, keeps the objects in an in-memory indexed store, and fires handlers on changes; shared means many controllers in the process reuse that one stream and cache. Cache reads are fast and free but eventually consistent, so they can be stale and must never be mutated.
solid answer
~1 minAn **informer** is the standard client-side machinery for watching a resource: a Reflector does list-then-watch, events land in a delta queue, and objects are stored in a thread-safe indexed cache that you query through a **Lister**. Event handlers usually just enqueue the object's key into a rate-limited workqueue, and the reconcile function reads current state back from the cache. **Shared** means one informer per resource type per process, with multiple controllers registering handlers on it. Without sharing, five controllers watching Pods would open five watches and hold five copies of every pod — memory and API-server load multiplied for nothing. The consequences of cache reads are what interviews probe. Reads are local, so they are fast and add zero API server load — that is why controllers must use Listers, not direct GETs, in hot paths. But the cache is **eventually consistent**: it can lag the API server by the watch delivery delay, so you may not see your own just-completed write, and you may act on an object that has already changed. Objects from the cache are **shared pointers**: mutating one corrupts every other consumer, so you must deep-copy before modifying. Writes always go to the API server, never to the cache.
code
go · 15 linesfactory := informers.NewSharedInformerFactory(clientset, 10*time.Minute)
podInformer := factory.Core().V1().Pods()
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
key, _ := cache.MetaNamespaceKeyFunc(obj)
queue.Add(key)
},
})
factory.Start(stopCh)
cache.WaitForCacheSync(stopCh, podInformer.Informer().HasSynced)
pod, err := podInformer.Lister().Pods(ns).Get(name) // local read, may be stale
updated := pod.DeepCopy() // never mutate the cached object
updated.Labels["phase"] = "active"
_, err = clientset.CoreV1().Pods(ns).Update(ctx, updated, metav1.UpdateOptions{})go deeper
Say an informer watches a resource, keeps a local copy, and calls your handlers on changes; reads come from memory.
Name the components (reflector, cache, lister, workqueue) and state the two rules: cache may be stale, never mutate cached objects.
Discuss operational consequences — memory footprint and how to trim it, staleness mitigation via optimistic concurrency and patches, what resync really does.
Reason about process-wide informer topology and control-plane load: which resources to cache, at what scope, and how staleness budgets shape controller correctness.
## The pieces - **Reflector** — performs LIST then WATCH against the API server and pushes changes into a queue, handling reconnects and 410 Gone relists. - **DeltaFIFO** — an ordered queue of per-object deltas so nothing is processed out of order for a given key. - **Indexer / Store** — a thread-safe in-memory map of the current objects, plus user-defined indexes (by namespace, by label, by owner) for cheap lookups. - **Lister** — the read API over the Indexer: Get(name), List(selector). - **Event handlers** — OnAdd / OnUpdate / OnDelete callbacks, which in a well-written controller do almost nothing except compute a key and enqueue it. - **Workqueue** — deduplicating and rate-limited, so a burst of updates for one object collapses into one reconcile, and failures are retried with backoff. A **SharedInformerFactory** ensures one informer exists per resource type (and per namespace/selector scope) in the process, with all consumers attached to it. ## Why the cache exists Controllers are level-triggered: on every reconcile they re-read current state. Doing that against the API server would mean a GET per reconcile per object per controller — a scaling disaster on a busy cluster. The informer converts that into one watch stream and unlimited local reads. This is the single biggest reason controllers scale at all. ## What you accept in return **Staleness.** The cache reflects what the watch has delivered. Typical lag is milliseconds, but it can grow under load, during API server rollouts, or if the client is slow to drain events. Two consequences follow directly: - *Read-your-own-write does not hold.* After a successful create or update, the cache may still return the old object. Reconcile logic must not assume its previous action is visible; it should be written so that acting again is harmless (idempotency) and that a missing effect simply triggers another pass. - *You may act on stale data.* The mitigation is not "read from the API server instead"; it is to make writes safe. Use optimistic concurrency (the resourceVersion in the object you write) so a stale-based update is rejected with 409 Conflict, or use a patch that expresses the intended change rather than a whole-object overwrite. **Shared, immutable-by-convention objects.** Everything the Lister returns is a pointer into the shared cache, visible to every other handler. Mutating it in place is a genuine and hard-to-debug bug: other controllers see phantom state, and your change is silently discarded on the next watch update. Always deep-copy before modifying and send the copy to the API server. **Memory cost.** The cache holds every object of the watched type in scope. Watching all Secrets or all Pods cluster-wide can be hundreds of megabytes on a large cluster. Reduce it by scoping the informer to a namespace, using label or field selectors, or installing a transform function that strips fields (managedFields, large annotations) before storage. **Resync is not a refetch.** A periodic resync replays the objects already in the cache through the handlers, so every object is reconciled again. It does not re-read the API server and will not repair a cache that missed data — that is what the Reflector's relist on 410 is for. Set it long (or zero) and rely on it as a safety net for missed reconciles, not as a consistency mechanism. ## The rules in short Read from the Lister, never GET in a hot loop; deep-copy before mutating; write to the API server and let 409 Conflict protect you from staleness; scope and trim informers to control memory; and let handlers enqueue keys rather than do work.
- Your controller creates a ConfigMap and immediately lists ConfigMaps from the informer cache, which does not include it. Is that a bug?Not in the cache — that is expected eventual consistency, since the object only appears after the watch event is delivered and processed. The bug would be logic that assumes it is there. Write the reconcile so a missing object simply means create-if-absent again, and make creation idempotent by handling AlreadyExists, letting the next pass converge.
- Why must you deep-copy an object obtained from a Lister before changing it?The Lister returns a pointer into the shared cache that every other handler in the process also reads. Mutating it in place makes other consumers observe state that does not exist on the server, and your change is lost anyway when the next watch update replaces the entry. A deep copy gives you a private object safe to modify and send to the API server.
A newsroom wire feed: one subscription to the wire, one archive everyone reads from. Reading the archive is instant but may miss the last few seconds, and marking up the office copy ruins it for every other reporter.
saying these in an interview costs you the question
- Treating the informer cache as strongly consistent or expecting read-your-own-write
- Mutating objects returned by a Lister without deep-copying
- Doing direct GET calls to the API server inside reconcile instead of using the Lister
- Thinking a short resync interval refreshes the cache from the API server
- Creating a separate informer per controller for the same resource type in one process
- Doing real work inside event handlers instead of enqueueing a key