Predicates and Event Filtering in controller-runtime

August 12, 2025

Predicates and Event Filtering in controller-runtime
Predicates and Event Filtering in controller-runtime

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.