KubernetesIV · Desired State and ReconciliationDesired state and reconciliation
Watch, informers, and events — how controllers observe the cluster
What you'll learn
- Trace the watch protocol from API server to informer cache
- Identify the parts of an informer: reflector, lister, indexer
- Distinguish Kubernetes Events from audit logs and from log streams
- Diagnose broken watches and event-storm scenarios
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
Controllers don’t poll the API server for every reconcile. They maintain a local cache fed by watch events. This lesson covers the watch protocol, the informer architecture, and the related concept of Kubernetes Events (the resource kind, not the OS events).
The watch protocol
A watch is a long-running HTTP request to the API server:
GET /api/v1/namespaces/prod/pods?watch=true&resourceVersion=128034
The server responds with the current state, then streams events as they happen. The client maintains a local cache that mirrors the server’s view.
sequenceDiagram
autonumber
participant API as API server
participant W as Watch client (informer)
W->>API: GET /pods?watch=true&resourceVersion=128034
API-->>W: initial state (list)
loop events
API-->>W: ADDED/MODIFIED/DELETED event
end
Note over W,API: connection drops
W->>API: re-list from last known resourceVersion
API-->>W: missing events since last seen
The watch protocol handles failures:
- Connection drop — client transparently re-lists from
the last known
resourceVersion; API server replays missing events. - 410 Gone — the requested
resourceVersionis too old (older than the watch cache retention); client must do a full re-list. - Slow consumer — if the client cannot keep up, the API server closes the watch; the client reconnects.
The informer architecture
The informer (in client-go) is the canonical implementation of the watch + cache + indexing pattern. It has three parts:
flowchart TB
API[API server] --> R[Reflector]
R -->|list + watch events| D[Delta FIFO queue]
D -->|pop events| P[Populator]
P --> I[Indexer<br/>thread-safe map]
I --> E[Event handlers]
E --> Q[Workqueue]
Q --> Rec[Reconciler]
Reflector
The reflector watches the API server. It:
- Lists resources on startup (gets the initial state)
- Opens a watch with the latest
resourceVersion - Forwards events to the Delta FIFO queue
- Re-lists on
410 Gone - Re-lists periodically (default every 10 minutes) to catch any missed events
The reflector is resilient to API server hiccups. A brief network outage does not cause a controller to lose state; on reconnect, the reflector re-lists and catches up.
Delta FIFO queue
A thread-safe FIFO that holds pending events. Each event is a delta — the change between the previous and current state. The informer processes events in order.
Indexer
A thread-safe map keyed by <namespace>/<name>. The reconciler
reads from the indexer, not from the watch stream. This
decouples reconcile latency from watch event rate.
The indexer supports indexers — secondary indexes by
label, annotation, or custom field. A controller can list
Pods with labels=app=web efficiently via the indexer.
Event handlers
Custom functions that react to events. The most common pattern:
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
key, _ := cache.MetaNamespaceKeyFunc(obj)
workqueue.Add(key)
},
UpdateFunc: func(old, new interface{}) {
key, _ := cache.MetaNamespaceKeyFunc(new)
workqueue.Add(key)
},
DeleteFunc: func(obj interface{}) {
key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
workqueue.Add(key)
},
})
The handler adds the object’s key to a workqueue. The reconciler dequeues, fetches the latest from the indexer, and reconciles.
Why informers are designed this way
The informer pattern solves several problems:
- API server load — without informers, every reconcile reads from the API server. With informers, reads hit the local cache. The API server is not flooded.
- Decoupling — the controller can take seconds to reconcile without affecting watch delivery.
- Resilience — a network blip does not cause the controller to lose state. The reflector re-lists.
- Atomic snapshots — the indexer provides a consistent view at the time of read.
Kubernetes Events (the resource)
Kubernetes Events are a resource kind (events.k8s.io/v1
or core/v1) that records things that happen in the cluster:
apiVersion: v1
kind: Event
metadata:
namespace: prod
name: web-abc.170c8e...
involvedObject:
kind: Pod
namespace: prod
name: web-abc
uid: ...
reason: FailedScheduling
message: '0/5 nodes are available: 3 Insufficient memory, 2 node(s) didn't match Pod's node affinity.'
type: Warning
source:
component: default-scheduler
firstTimestamp: 2026-08-15T12:01:01Z
lastTimestamp: 2026-08-15T12:01:01Z
count: 1
Events are emitted by:
- The scheduler (FailedScheduling, Scheduled)
- kubelet (FailedMount, Pulling, Created, Started)
- Controllers (ReplicaSet created, Deployment updated)
- Admission webhooks (with custom messages)
- The Node controller (Node Ready, Node NotReady)
Events are not durable long-term: they live in etcd but
are pruned (default 1 hour retention for events.k8s.io/v1,
5 minutes for older core/v1). Production clusters ship
events to a central store (Loki, Elasticsearch) for long-term
retention.
kubectl get events
kubectl get events -A --sort-by='.lastTimestamp'
LAST SEEN TYPE REASON OBJECT MESSAGE
2m Warning FailedScheduling pod/web-abc 0/5 nodes are available: insufficient memory
3m Normal Scheduled pod/db-xyz Successfully assigned db-xyz to worker-04
5m Warning BackOff pod/api-123 Back-off pulling image my-app:v3
10m Normal Pulled pod/api-123 Successfully pulled image my-app:v3
15m Normal Started pod/api-123 Started container app
kubectl describe <object> includes the events for that
object:
kubectl describe pod web-abc | grep -A 20 "Events:"
The Pod events tell the reconciliation story: why it was scheduled, why an image was pulled, why a container started or failed.
Event-storm scenarios
A common operational problem is an event storm: many events of the same kind in a short time, often from a single recurring failure.
sequenceDiagram
autonumber
participant C as Controller (broken)
participant API as API server
loop every N seconds
C->>API: PATCH (fails)
API-->>C: 409 Conflict
C->>API: re-list (no progress)
C->>API: emit Event: "ReconcileFailed"
end
Note over API: thousands of events/minute
Production consequences:
- The events fill the API server’s storage
- The events fill the cluster’s etcd (auto-pruned but still costly)
- The events fill monitoring pipelines
Defensive patterns:
- Coalesce repeated events: only emit a new Event if the state changed since the last
- Suppress events in the controller for known-harmless retries
- Rate-limit event emission at the controller level
- Filter events in the log pipeline before shipping
Watch latency
Watch latency is the time from a state change to the informer cache being updated. Production monitoring:
# API server metrics
apiserver_watch_events_total
apiserver_watch_duration_seconds
A healthy watch latency is < 100 ms p99. Sustained high latency (> 1 second) indicates:
- API server overloaded
- etcd commit latency high
- Network between API server and etcd slow
Diagnosing broken watches
A controller that is not picking up changes may have:
- Lost the watch — the reflector’s watch was dropped and re-list is failing
- Indexer out of sync — events are arriving but the indexer is not being updated (a bug)
- Event handler broken — events are arriving but the workqueue is not being populated
Diagnostic steps:
# 1. Is the API server healthy?
kubectl get --raw /healthz
# 2. Is the watch returning events?
kubectl get pods -w --v=8 2>watch.log
# (in another terminal)
kubectl delete pod web-abc
# Confirm an event appears in watch.log
# 3. Is the controller's workqueue growing?
controller.workqueue_depth
Cross-course references
- The Linux course part
VI-Linux-Processescovers the process state transitions that map onto informer events. - The Observability course part
IX-Observability-Exporterscovers the metrics surface that exposes watch latency. - The Linux course part
XXIV-Linux-Timecovers chrony — watch timestamps depend on clock accuracy. - The Docker course part
XXX-Docker-Lifecyclecovers the container lifecycle that drives much of the event stream.
Quiz
Knowledge check · 4 questions
Q1. What are the three parts of a Kubernetes informer?
Q2. Kubernetes Events are durable and retained for at least 24 hours by default.
Q3. A team's cluster has millions of Events in etcd, causing etcd latency spikes. The dominant event reason is `BackOff` (image pull backoff) on a single Pod. The Pod keeps crash-looping, the kubelet keeps retrying, and every retry emits a new Event. Diagnose and remediate.
Top event reasons: ``` $ kubectl get events -A --sort-by='.count' | tail -5 LAST SEEN TYPE REASON COUNT OBJECT 2m Warning BackOff 58432 pod/web-abc-1 3m Warning BackOff 58221 pod/web-abc-1 5m Warning Failed 29110 pod/web-abc-1 7m Warning Failed 28901 pod/web-abc-1 10m Warning ImagePullBackOff 14500 pod/web-abc-1 ``` Pod status: ``` $ kubectl get pod web-abc-1 NAME READY STATUS RESTARTS AGE web-abc-1 0/1 ImagePullBackOff 124 15m ``` Pod events (first 3): ``` Warning Failed Error: ImagePullBackOff: Back-off pulling image "my-app:v3" Warning Failed Error: ImagePullBackOff: Back-off pulling image "my-app:v3" Warning Failed Error: ImagePullBackOff: Back-off pulling image "my-app:v3" ``` etcd DB size: ``` $ etcdctl endpoint status --write-out=table | ... | DB SIZE | 8.4 GB | ... | ```
Q4. Explain how the informer pattern handles a network blip between the API server and the controller. What happens during the blip and after?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Ship events to a central store with long retention; default etcd retention is too short for incident investigation.
- Defend against event storms: rate-limit event emission in controllers; coalesce repeated events.
- Monitor watch latency (
apiserver_watch_duration_seconds) on the API server. - Treat informer events as advisory: the reconciler should re-read the indexer, not trust the event payload.
- Audit
metadata.managedFieldsto identify which controller owns each object’s state.