Skip to main content

Watch Strategy

The watch strategy controls how the operator creates watch connections to the Kubernetes API server. It is set via OperatorSettings.WatchStrategy and defaults to PerController.

Each controller narrows the objects it watches with a label and/or field selector (see Controllers). The watch strategy decides whether every controller gets its own connection with the selector applied server-side, or whether controllers over the same entity type share a single connection and are matched client-side.

PerController (Default)

Every registered controller gets its own watch connection with its label and field selectors applied server-side — the API server only delivers matching objects:

builder.Services
.AddKubernetesOperator(); // WatchStrategy defaults to WatchStrategy.PerController

This works out of the box and is the most efficient mode for the common cases: one controller per entity type, or multiple controllers with disjoint selectors over large object sets (each watcher only receives its slice). The cost scales with the controller count: N controllers on the same entity type maintain N watch connections, and overlapping selectors mean events are transferred and deduplicated once per controller.

When to use PerController

You have one controller per entity type, or a small number of controllers whose selectors rarely overlap, or a single narrow selector over a very large object set (server-side filtering keeps the transferred data minimal).

SharedPerEntity (Optimized for Many Controllers)

One shared watch connection per entity type. Events are deduplicated once and then dispatched to every controller whose label selector matches the entity, evaluated client-side:

builder.Services
.AddKubernetesOperator(settings => settings
.WithWatchStrategy(WatchStrategy.SharedPerEntity));

Compared to PerController this reduces API server connections and deduplication cache entries to one per entity type, independent of the controller count. Choose it when many controllers watch the same entity type, especially with overlapping selectors or a high number of objects.

When to use SharedPerEntity

Many controllers watch the same entity type — for example a set of controllers each handling a different label-selected slice of the same resource — and the overlap or connection count under PerController becomes a bottleneck.

Trade-offs
  • With multiple controllers per entity type, the shared watch runs without a server-side label selector, so the operator receives all objects of that type. A single narrow selector over a huge object set is cheaper with PerController.
  • If only one controller is registered for an entity type, the shared strategy automatically falls back to a dedicated watcher with server-side selectors — there is nothing to share.
  • Controllers with a field selector always keep a dedicated watch connection; field selectors cannot be evaluated client-side.
  • Client-side matching parses the selector string returned by IEntityLabelSelector<TEntity> (full standard syntax: equality, set-based, existence). To skip parsing or apply custom matching logic, implement IClientSideEntitySelector<TEntity> on the selector — the shared watcher then calls MatchesAsync(entity, cancellationToken) directly.

Label transitions

When an object's labels change so that it starts or stops matching a controller's selector, both watch strategies deliver the transition — with parity across ReconcileStrategy.ByGeneration and ReconcileStrategy.ByResourceVersion:

  • Entry (an object starts matching a controller): the controller receives an Added — under PerController from its server-side filtered watch, under SharedPerEntity synthesized by the shared dispatcher, which evaluates membership before deduplication so a label-only change is never dropped.
  • Exit (an object stops matching a controller): the controller receives a Deleted, invoking DeletedAsync — again from the server-side watch under PerController, and synthesized under SharedPerEntity.

Steady-state events for objects that keep matching still go through the normal reconcile-strategy deduplication, so ByGeneration continues to skip label- and annotation-only writes for controllers whose membership did not change.

note

Neither strategy delivers a transition that happened while the watch was disconnected: on reconnect the current state is re-listed (rebuilding SharedPerEntity membership), but an object that left a selector during the gap produces no synthetic Deleted — identical to a server-side filtered watch.