Why Salesforce trigger recursion happens
If you’ve spent any time in a complex org, you’ve probably run into the nightmare of Salesforce trigger recursion. It’s that moment when your code updates a record, which fires a trigger, which updates the record again, and suddenly you’re staring at a “Maximum stack depth reached” error. It’s frustrating, but it’s a common hurdle for every developer.
Look, it usually boils down to a few things. Maybe you’re performing an update on the same object inside the trigger handler. Or maybe you have a chain reaction where Object A updates Object B, which then updates Object A again. In my experience, teams often lose days trying to untangle these loops because they didn’t have a clear guard strategy from day one.
Common causes of the loop
- Updating the same record in an after update trigger without checking if anything actually changed.
- Cross-object updates that circle back to the original record.
- Multiple triggers and workflows fighting for control over the same record.

Best ways to stop Salesforce trigger recursion
So how do we fix this? There isn’t just one “right” way, but there are definitely some patterns that work better than others depending on your specific scenario. Let’s break down the tools we have in the toolbox. The goal is to make your code predictable and safe for bulk operations.
1. The static Boolean flag (The simple guard)
This is the most common approach I see. Since static variables live for the entire transaction, you can use a Boolean to track if the trigger has already run. It’s easy to set up, but honestly, it’s a bit of a blunt instrument. It’s great for simple logic, but it can cause issues in complex transactions.
public class TriggerHandler {
public static Boolean isFirstRun = true;
}
trigger AccountTrigger on Account (after update) {
if (TriggerHandler.isFirstRun) {
TriggerHandler.isFirstRun = false;
// Your logic here
}
}But here’s the thing: this can actually bite you. If you’re processing 200 records and then another 200 in the same transaction, that second batch might get skipped entirely. I’ve seen this cause data issues in bulk uploads, so use it with caution and only when you know the transaction lifecycle inside and out.
2. Using a Set of IDs for record-level control
If you need something more precise, a Set of IDs is the way to go. Instead of blocking the whole trigger, you only block the specific records that have already been processed. This is much safer for bulk operations and is usually what I recommend for enterprise-level orgs.
Pro tip: Always clear your static sets in your test classes if you’re running multiple DML statements in one test method, or your tests might fail for the wrong reasons.
public class AccountState {
public static Set<Id> processedIds = new Set<Id>();
}
trigger AccountTrigger on Account (after update) {
List<Account> toProcess = new List<Account>();
for (Account acc : Trigger.new) {
if (!AccountState.processedIds.contains(acc.Id)) {
toProcess.add(acc);
}
}
if (toProcess.isEmpty()) return;
// Logic here...
AccountState.processedIds.addAll(toProcess.keySet());
}3. Check for actual field changes
One thing that trips people up is running logic even when the relevant fields haven’t changed. Before you do any DML, compare Trigger.oldMap with Trigger.newMap. If the “Status” field didn’t change, why are you running the “Status Update” logic? This simple check prevents a lot of unnecessary Salesforce trigger recursion.
If you’re still deciding between using code or a declarative tool, you might want to check out my guide on Apex vs Flow. Sometimes a Flow can handle the logic without the same recursion headaches, but code gives you more control over the state when things get messy.
4. Move work to Asynchronous Apex
Sometimes the best way to avoid a loop is to just step out of the current transaction. By using a Queueable or a Future method, you start a fresh transaction with its own limits. This is a solid move for heavy processing that doesn’t need to happen instantly, like calling an external API or updating thousands of related records.
We’ve talked before about how to manage Asynchronous Apex, and it’s a lifesaver here. Just remember that you can’t fire a Future method from another Future method, so you still need to plan your architecture carefully to avoid hitting different limits.
Key Takeaways
- Salesforce trigger recursion happens when DML operations re-fire the same trigger logic in a loop.
- Static Booleans are simple to write but can accidentally skip records during bulk processing.
- Static Sets of IDs are the gold standard for record-level recursion protection.
- Always compare old and new field values to see if the update is even necessary before running logic.
- Consider moving logic to a Salesforce Apex Trigger handler class to keep your code organized and testable.
Handling recursion isn’t just about stopping errors; it’s about making your org predictable. When I first started, I thought a single Boolean was enough for everything. I quickly learned that as an org grows, you need a more nuanced approach. Start with field-level checks, move to ID sets when needed, and always keep an eye on your debug logs to see exactly how many times your code is firing. It’ll save you a lot of stress during your next big deployment.








Leave a Reply