Reconcile Strategy
The reconcile strategy controls which watch events actually trigger a reconciliation cycle.
It is set via OperatorSettings.ReconcileStrategy and defaults to ByGeneration.
Background: Generation vs. ResourceVersion
Every Kubernetes resource carries two version counters in metadata:
| Field | Incremented by | When |
|---|---|---|
metadata.generation | The API server | On spec changes; never on .metadata changes (labels, annotations); not on status changes when a status subresource is enabled |
metadata.resourceVersion | The API server | On every successful write (spec, status, labels, annotations, finalizers, …) |
Understanding this distinction is key to choosing the right strategy.
ByGeneration (Default)
builder.Services
.AddKubernetesOperator(settings => settings
.WithReconcileStrategy(ReconcileStrategy.ByGeneration));
The watcher caches the last seen metadata.generation for each resource. A watch event triggers
reconciliation only when the incoming generation is greater than the cached one.
Consequences:
- Label or annotation changes do not trigger reconciliation (they are
.metadatachanges and never incrementgeneration). - Status updates do not trigger reconciliation when the CRD has a status subresource enabled.
- Finalizer additions/removals during deletion bypass the check because
DeletionTimestampis set — deletion does not incrementgeneration, so bypassing is required to handle finalizers correctly.
This matches the pattern used by most Kubernetes controllers (e.g., kube-controller-manager)
and is the correct default for the majority of operators.
ByGenerationYour controller only reacts to .spec changes. Status writes, label updates, and annotation
changes are either performed by your own controller (and should not re-trigger it), or performed
by other actors and are irrelevant to your business logic.
ByResourceVersion
builder.Services
.AddKubernetesOperator(settings => settings
.WithReconcileStrategy(ReconcileStrategy.ByResourceVersion));
The watcher caches the last seen metadata.resourceVersion (a string). A watch event triggers
reconciliation whenever the incoming resourceVersion differs from the cached one — which is true
for every successful API server write, regardless of which field changed.
Consequences:
- Status updates do trigger reconciliation.
- Label or annotation changes do trigger reconciliation.
- Finalizer removals during deletion trigger reconciliation naturally without any special bypass,
because each finalizer removal is a real API write that increments
resourceVersion. - Your controller must be idempotent for status-only changes, since it will be called when
another actor updates
.statuson the same resource.
Because every write to the resource increments resourceVersion, a reconciler that writes
back to the resource — for example to update .status — will trigger a new watch event, which
triggers another reconcile, which writes again, and so on.
The official Kubernetes level-driven controller pattern addresses this: a controller must always work from current observed state, not from the event that triggered it. In practice: only write when the value actually changed.
// Compute the desired state from the entity
var desired = ComputeDesired(entity);
// Guard: only write when the resource actually needs to change.
// Any write increments resourceVersion, triggers a new watch event, and
// therefore a new reconcile — omitting this guard causes an infinite loop.
if (NeedsToBeUpdated(entity, desired))
{
await ApplyUpdate(entity, desired, cancellationToken);
}
Without this guard, ByResourceVersion will produce an infinite reconcile loop for any
controller that writes to the resource during reconciliation.
ByResourceVersionYour controller needs to react to changes outside .spec. Common examples:
- A controller that mirrors label or annotation values into child resources.
- A controller that acts on status conditions written by another controller.
- A controller that must respond to ownership or finalizer changes made by external actors.
ByResourceVersion can significantly increase the number of reconciliation calls, especially for
resources whose .status is updated frequently. Size MaxParallelReconciliations and the error
backoff settings accordingly (see Parallel Reconciliation) and ensure
your controller logic is fast for status-only no-op cases.