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.
PerControllerYou 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.
SharedPerEntityMany 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.
- 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, implementIClientSideEntitySelector<TEntity>on the selector — the shared watcher then callsMatchesAsync(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— underPerControllerfrom its server-side filtered watch, underSharedPerEntitysynthesized 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, invokingDeletedAsync— again from the server-side watch underPerController, and synthesized underSharedPerEntity.
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.
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.