Skip to content

Trigger Loop Prevention

Overview

When an event-driven trigger fires a function that modifies records in the same collection, it can create an infinite loop — the function's write fires the trigger again, which fires the function again, and so on. Centrali prevents this with three layers of protection: static analysis, runtime detection, and rate limiting.

Trigger loop prevention applies to event-driven triggers only. Scheduled triggers and HTTP triggers are not affected.

Layer 1: Static Analysis

When you create or update an event-driven trigger, Centrali scans the linked function's code for SDK calls that would re-fire the trigger.

For example, if you create a trigger on record_updated for the orders collection, and the function calls api.updateRecord('orders', ...), that's a self-loop — the update would fire the trigger again.

What's checked

The analyzer maps each event type to the SDK methods that would produce that event:

Event Type SDK Methods That Would Re-Fire
record_created createRecord, upsertRecord, batchCreateRecords, bulkCreateRecords
record_updated updateRecord, upsertRecord, incrementField, decrementField, batchUpdateRecords, bulkUpdateRecords
record_deleted deleteRecord, batchDeleteRecords, bulkDeleteRecords
records_bulk_created bulkCreateRecords
records_bulk_updated bulkUpdateRecords
records_bulk_deleted bulkDeleteRecords

record_restored and record_expired events cannot be triggered by SDK methods, so they are not checked.

When it runs

Static analysis runs when you:

  • Create an event-driven trigger
  • Update a trigger's function, event type, or collection
  • Re-enable a paused trigger
  • Update the code of a function that has linked triggers

What happens if a loop is detected

At trigger creation, the console shows a "Trigger loop detected" dialog listing the conflicting SDK calls. You can go back and fix the function, or choose a different collection.

If a loop is detected when you update function code, the system auto-pauses any affected triggers to prevent the loop from firing.

Cross-collection writes are allowed

Static analysis only blocks writes to the same collection the trigger listens on. A trigger on orders.record_updated that calls api.createRecord('audit_logs', ...) is fine — different collections don't create loops.

Layer 2: Runtime Detection

Even if static analysis doesn't catch a loop (for example, if the collection is determined dynamically at runtime), a second layer of protection exists.

Every record event carries a sourceTriggerId that tracks which trigger started the chain. Before firing a trigger, the system checks: if the event's sourceTriggerId matches the trigger's own ID, the trigger is skipped.

This propagates through all record operations — single, batch, and bulk — so indirect loops through multiple function calls are also caught.

Layer 3: Rate Limiting

As a final safety net, each event-driven trigger has a per-trigger rate limit.

Setting Default
Max executions 500 per 60 seconds
Storage Redis sliding window

If a trigger exceeds the rate limit, it is automatically paused and all further executions are skipped until an admin re-enables it.

If Redis is unavailable, the rate limiter fails open (allows execution) to avoid blocking legitimate triggers.

Auto-Pause

Triggers are automatically paused (disabled) in two situations:

  1. Rate limit exceeded — the trigger fired too many times in the rate window
  2. Function code update — the linked function was updated and now contains a self-loop

When a trigger is auto-paused, it appears as paused in the console with the reason recorded. You can re-enable it after fixing the underlying issue.

What's Covered

All record mutation paths propagate loop detection:

Single operations: createRecord, updateRecord, deleteRecord, incrementField, decrementField

Batch operations: batchCreateRecords, batchUpdateRecords, batchDeleteRecords — these fire individual events per record

Bulk operations: bulkCreateRecords, bulkUpdateRecords, bulkDeleteRecords — these fire a single aggregate event