Skip to main content

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:

FieldIncremented byWhen
metadata.generationThe API serverOn spec changes; never on .metadata changes (labels, annotations); not on status changes when a status subresource is enabled
metadata.resourceVersionThe API serverOn 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 .metadata changes and never increment generation).
  • Status updates do not trigger reconciliation when the CRD has a status subresource enabled.
  • Finalizer additions/removals during deletion bypass the check because DeletionTimestamp is set — deletion does not increment generation, 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.

When to use ByGeneration

Your 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 .status on the same resource.
Infinite reconcile loop risk

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.

When to use ByResourceVersion

Your 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.
Performance consideration

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.