Level: AdvancedSalesforce

Apex Trigger Recursion: How to Prevent Endless Loops

9 min read
Apex Trigger Recursion featured image: title card with an Apex code example

Apex Trigger Recursion happens when a trigger causes its own object to be saved again, so the same trigger runs again and again. Sometimes it only wastes limits. Sometimes it loops until Salesforce stops it with a “maximum trigger depth exceeded” error.

In this post, you will learn what causes recursion, how to spot it, and the safe ways to prevent it, including why a simple static Boolean is often the wrong fix. It builds on the Apex trigger handler pattern, before vs after triggers and static vs instance Apex, because static variables play a big part here.

What is Apex Trigger Recursion?

Apex Trigger Recursion is a trigger running again inside the same transaction because of its own work. For example, an after update trigger on Course__c updates the same courses. That update fires the after update trigger again, which updates the courses again, and so on.

Think of two mirrors facing each other. Each reflects the other, which reflects the first, forever. A trigger that updates its own records creates the same endless reflection.

Why Apex Trigger Recursion Matters

Recursion causes errors, wrong data and wasted limits. Here is why Apex Trigger Recursion matters at Bright Path Tutors.

  • First, a course trigger that stamps Last Fee Change re-runs on its own update.
  • Also, an enrolment trigger updates the course, and a course trigger updates its enrolments, which loops between objects.
  • Next, a workflow field update fires update triggers one more time.
  • Some flows update records that then fire triggers again.
  • Finally, duplicate emails or tasks appear when logic runs twice.

So every developer must know how to stop unwanted repeats.

How Apex Trigger Recursion Happens

Apex Trigger Recursion follows a loop. First, a user updates a course, and the after update trigger runs. Next, the trigger runs an update DML on the same courses. Then that update fires the after update trigger again. After that, the loop continues until the logic stops or Salesforce reaches the maximum trigger depth of 16. Finally, at that point, Salesforce throws an exception and rolls back everything.

Apex Trigger Recursion diagram showing a trigger update loop that reaches maximum depth 16
How a trigger that updates its own records loops.

Direct Recursion

The trigger updates its own records, usually in an after trigger. This is the most common case.

Indirect Recursion

Two objects update each other. For example, Enrolment updates Course, and Course updates Enrolment.

Re-Runs from Other Automation

Workflow field updates and some flows save the record again. So update triggers run another time in the same transaction.

Chunks of 200 Records

A save of 500 records runs the trigger in chunks of 200, 200 and 100. Static variables keep their values across these chunks, which matters for recursion guards.

Ways to Prevent Apex Trigger Recursion

The table below compares the common prevention techniques.

TechniqueHow it worksRisk
Use before triggersSet fields without DMLNone, the best first choice
Check changed fieldsAct only when oldMap differsLow
Static Set of processed IdsSkip records already handledLow, the recommended guard
Static Boolean flagRun only once per transactionHigh, skips later chunks
Bypass flag in handlerTurn logic off during own DMLMedium, must reset correctly

Static Boolean vs Static Set of Ids

The table below compares the two most common guards.

FeatureStatic BooleanStatic Set of Ids
TracksWhether the trigger ran at allWhich records were processed
Works for 500 recordsNo, chunks 2 and 3 are skippedYes
Allows a later valid changeNoYes, for new records
Code sizeSmallestSmall
RecommendedRarelyYes

When to use which is clear. Prefer changed-field checks first, then add a static set of Ids when you still need a guard. Avoid a single static Boolean.

Apex Trigger Recursion diagram comparing a static Boolean guard and a static Set of Ids
Static Boolean vs static Set of Ids.

A Real Apex Trigger Recursion Example

Let’s fix Apex Trigger Recursion at Bright Path Tutors. When a course fee changes, an after update trigger stamps Last_Fee_Change__c on the course.

Step 1: The Recursive Version

// BAD: updates the same records in an after trigger
trigger CourseTrigger on Course__c (after update) {
    List<Course__c> toStamp = new List<Course__c>();
    for (Course__c c : Trigger.new) {
        toStamp.add(new Course__c(Id = c.Id, Last_Fee_Change__c = System.now()));
    }
    update toStamp;   // fires this trigger again, and again
}

Every update fires the trigger, and the trigger updates the courses again. The loop stops only when Salesforce reaches the maximum trigger depth.

Step 2: Best Fix, Use a Before Trigger

trigger CourseTrigger on Course__c (before update) {
    for (Course__c c : Trigger.new) {
        if (c.Fee__c != Trigger.oldMap.get(c.Id).Fee__c) {
            c.Last_Fee_Change__c = System.now();
        }
    }
}

The before trigger sets the field directly, so no DML runs and no recursion is possible. Also, the changed-field check stamps only courses whose fee really changed.

Step 3: When You Need a Guard, Use a Static Set

Sometimes after-trigger logic must update related records that update the parent back. Then use a static set of processed Ids.

public class TriggerGuard {
    private static Set<Id> processed = new Set<Id>();

    // Returns only the records not yet handled in this transaction
    public static List<SObject> firstTime(List<SObject> records) {
        List<SObject> fresh = new List<SObject>();
        for (SObject r : records) {
            if (!processed.contains(r.Id)) {
                processed.add(r.Id);
                fresh.add(r);
            }
        }
        return fresh;
    }
}

A static variable lives for the whole transaction. So when the trigger runs again, firstTime returns only records it has not seen. Records in the second and third chunks of a large load are still new, so they are processed correctly.

Step 4: Use the Guard in the Handler

when AFTER_UPDATE {
    List<Course__c> fresh = (List<Course__c>) TriggerGuard.firstTime(Trigger.new);
    if (!fresh.isEmpty()) {
        CourseService.syncEnrolmentFees(fresh, (Map<Id, Course__c>) Trigger.oldMap);
    }
}

The handler passes only fresh records to the service. If the enrolment update causes the course trigger to run again, the guard returns an empty list, and the loop ends.

Why a Static Boolean Fails

Many tutorials show static Boolean hasRun = false. It stops recursion, but it also stops the trigger for every later chunk in the same transaction. So in a load of 500 courses, only the first 200 get processed, and the other 300 silently skip the logic. That bug is very hard to find.

Common Mistakes with Apex Trigger Recursion

  • Updating the same records in an after trigger to set a field.
  • Using a single static Boolean as a recursion guard.
  • Forgetting that workflow field updates re-run update triggers.
  • Acting on every update instead of checking changed fields.
  • Never testing with more than 200 records.

Best Practices for Apex Trigger Recursion

  • Set fields on the same record in before triggers.
  • Act only when relevant fields change.
  • Use a static set of processed Ids when a guard is needed.
  • Keep guards in one place, such as the handler.
  • Test with 201 or more records to cover several chunks.

Practice Task

Write a test that inserts 450 courses and updates all their fees in one statement. Then assert that all 450 have Last_Fee_Change__c set. Hint: this test would fail with a static Boolean guard and pass with the static set.

Most Asked Interview Questions on Apex Trigger Recursion

Questions on Apex Trigger Recursion separate junior from senior developers. Here are the 21 questions asked most about Apex Trigger Recursion.

Basic Questions on Apex Trigger Recursion

Q1: What is trigger recursion?

It is a trigger running again in the same transaction because of its own DML or other automation.

Q2: What is the maximum trigger depth?

  1. After that, Salesforce throws an exception and rolls back the transaction.

Q3: What usually causes recursion?

An after trigger that updates its own records, or two objects that update each other.

Q4: How does a before trigger help?

It sets fields without DML, so it never fires the trigger again.

Q5: Why do static variables help with recursion?

They keep their value for the whole transaction, so later trigger runs can see what earlier runs did.

Q6: Do workflow field updates cause triggers to run again?

Yes. A workflow field update fires before and after update triggers one more time.

Q7: What is indirect recursion?

When two objects update each other, such as a parent and its children, in a loop.

Code and Scenario Questions on Apex Trigger Recursion

Q8: Why is a static Boolean a weak guard?

It blocks every later chunk in the same transaction, so records after the first 200 are skipped.

Q9: What is a better guard?

A static Set of processed record Ids, so only records already handled are skipped.

Q10: How do changed-field checks prevent recursion?

They run logic only when a field really changes. The second run sees no change, so it does nothing.

Q11: How do you stamp a date when a field changes?

In a before update trigger, compare with Trigger.oldMap and set the date field directly.

Q12: How would you test recursion handling?

Insert and update more than 200 records in one statement and assert that every record was processed once.

Q13: Where should recursion guards live?

In one central place, such as the trigger handler or a small guard class.

Q14: Can a flow cause trigger recursion?

Yes. An after-save flow that updates the same record can fire update triggers again.

Advanced Questions on Apex Trigger Recursion

Q15: Do static variables reset between trigger chunks?

No. They keep their values for the whole transaction, across all chunks.

Q16: Why can a static set of Ids still miss a valid update?

If a record legitimately changes twice in one transaction, the second change is skipped. So combine the guard with changed-field checks.

Q17: How do you handle recursion between parent and child triggers?

Use changed-field checks on both sides and a shared guard, and update only records whose values actually differ.

Q18: Can you reset a static guard?

Yes, by clearing the set, but do it carefully. Resetting at the wrong time brings the loop back.

Q19: What happens to static variables in test methods?

Each test method runs in its own transaction, so static variables start fresh in each test.

Q20: How does recursion affect governor limits?

Each extra run uses more queries, DML and CPU time, so recursion often causes limit errors before reaching the maximum depth.

Q21: What is the safest overall strategy?

Prefer before triggers, check changed fields, avoid same-record DML in after triggers, and use a static set guard only when needed.

Official Resources

Quick Recap

  • Apex Trigger Recursion is a trigger re-running because of its own work.
  • Salesforce stops loops at a maximum trigger depth of 16.
  • Before triggers and changed-field checks prevent most recursion.
  • A static set of processed Ids is safer than a static Boolean.
  • Test with more than 200 records to cover every chunk.

What’s Next?

Not every rule needs a trigger. Next, read our guide to Apex Trigger vs Flow to choose the right tool.