Your controller doesn’t need to reconcile every change. Predicates let you filter out the noise.
Estimated Reading Time : 7m
The problem
By default, a controller reconciles on every create, update, and delete event for its watched resource type. For a ConfigMap controller in a cluster with hundreds of ConfigMaps, that means reconciling on every annotation change, label update, and status modification — even ones you don’t care about.
Predicates are filters that run before an event reaches the work queue. If the predicate returns false, the event is dropped.
Built-in predicates
controller-runtime provides several predicates out of the box.
GenerationChangedPredicate
The most commonly used predicate. It only triggers reconciliation when the .metadata.generation field changes — which happens when the spec changes, but not when status, labels, or annotations change.
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.Backup{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Complete(reconciler)
This eliminates the most common source of unnecessary reconciles: status updates from your own controller triggering another reconcile.
LabelSelectorPredicate
Only reconcile resources that match a label selector:
labelPredicate, _ := predicate.LabelSelectorPredicate(metav1.LabelSelector{
MatchLabels: map[string]string{"managed-by": "my-controller"},
})
ctrl.NewControllerManagedBy(mgr).
For(&corev1.ConfigMap{}, builder.WithPredicates(labelPredicate)).
Complete(reconciler)
Now your controller ignores ConfigMaps that don’t have the managed-by: my-controller label.
AnnotationChangedPredicate
Only reconcile when annotations change. Useful if your controller stores operational data in annotations.
builder.WithPredicates(predicate.AnnotationChangedPredicate{})
Custom predicates
When the built-in predicates aren’t enough, implement the predicate.Predicate interface:
type Predicate interface {
Create(event.CreateEvent) bool
Update(event.UpdateEvent) bool
Delete(event.DeleteEvent) bool
Generic(event.GenericEvent) bool
}
Each method returns true (process the event) or false (skip it).
Example: only reconcile on data changes
A predicate that ignores ConfigMap updates unless the actual .data field changed:
type dataChangedPredicate struct {
predicate.Funcs
}
func (p dataChangedPredicate) Update(e event.UpdateEvent) bool {
oldCM, ok := e.ObjectOld.(*corev1.ConfigMap)
if !ok {
return false
}
newCM, ok := e.ObjectNew.(*corev1.ConfigMap)
if !ok {
return false
}
return !reflect.DeepEqual(oldCM.Data, newCM.Data)
}
Embedding predicate.Funcs provides default implementations (return true) for the methods you don’t override.
Example: ignore resources in a specific namespace
type ignoreNamespace struct {
predicate.Funcs
Namespace string
}
func (p ignoreNamespace) Create(e event.CreateEvent) bool {
return e.Object.GetNamespace() != p.Namespace
}
func (p ignoreNamespace) Update(e event.UpdateEvent) bool {
return e.ObjectNew.GetNamespace() != p.Namespace
}
func (p ignoreNamespace) Delete(e event.DeleteEvent) bool {
return e.Object.GetNamespace() != p.Namespace
}
Combining predicates
Use predicate.And, predicate.Or, and predicate.Not to compose predicates:
ctrl.NewControllerManagedBy(mgr).
For(&corev1.ConfigMap{}, builder.WithPredicates(
predicate.And(
labelPredicate,
predicate.Not(ignoreNamespace{Namespace: "kube-system"}),
),
)).
Complete(reconciler)
This reconciles ConfigMaps that have the right label AND are not in kube-system.
When to use predicates
Always use GenerationChangedPredicate for custom resources with a status subresource. Without it, updating status triggers another reconcile, which updates status again — an infinite loop throttled only by rate limiting.
Use label predicates when your controller manages a subset of a built-in resource type. This is common for controllers that watch ConfigMaps, Secrets, or Services owned by a specific application.
Use custom predicates when you need fine-grained control over which changes matter. Comparing specific fields (like .data on a ConfigMap) avoids reconciling on irrelevant metadata changes.
Don’t over-filter. If you’re unsure whether an event matters, let it through — the reconciler should be idempotent anyway. Predicates are an optimization, not a correctness mechanism.