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.