Number of iterations exceeded. The flow can't perform more than 2,000 loop iterations.
A Flow `Loop` element ran more than 2,000 iterations. The fix is to operate on the *collection* directly (Update Records, Get Records with filters, or assignment to a record collection) instead of iterating record-by-record.
Also seen asNumber of iterations exceeded·2,000 loop iterations·flow loop iteration limit·loop iterations exceeded flow
A scheduled flow recalculates the renewal status for every active subscription on the first of each month. The org has 8,500 subscriptions. The May 1st run completes. The June 1st run dies halfway with "Number of iterations exceeded. The flow can't perform more than 2,000 loop iterations." The flow has been running for three years without issues. The team needs to understand the limit, why the run that completed last month fails this month, and how to restructure the flow.
What the platform is checking
Flows in Salesforce enforce a maximum of 2,000 iterations per loop element within a single flow run. The limit applies to the number of times a Loop element advances its iterator, regardless of the input collection's size. When a Loop element would advance to its 2,001st iteration, the runtime halts the flow with the iteration-limit error.
The limit exists for two reasons. The first is governor safety. Each loop iteration can execute additional flow elements that consume CPU, SOQL, and DML budgets. An unbounded loop multiplies those costs and can crash an entire transaction. The 2,000-iteration ceiling forces flows to think about chunking and asynchronous splits for any workload that crosses the boundary.
The second is design pressure. A flow that needs to iterate over more than 2,000 records is almost always doing work that belongs in a different paradigm: Batch Apex for record-by-record processing, a scheduled job for time-spaced work, or a record-triggered flow for per-record reactions to data changes.
The limit applies per Loop element. A flow with two separate loops can each run up to 2,000 iterations, but a single loop element cannot exceed 2,000 even if you reset its state.
The broken example
A scheduled-triggered flow whose entry condition is "Subscription where Status equals Active". The flow's high-level structure:
- Get Records: Subscriptions where Status = 'Active'.
- Loop: iterate over each Subscription.
- Decision: is the renewal date within 30 days?
- Update Records: set Subscription.Renewal_Notice_Sent = true.
- End Loop.
The flow ran fine when the org had 1,500 active subscriptions. When the active count crossed 2,000, the loop element hits its ceiling and the flow halts. The remaining subscriptions are never processed.
A second shape: a record-triggered flow on Opportunity that loops over OpportunityLineItems to update related Pricebook entries. For most Opportunities, the line-item count is small. For a few large enterprise Opportunities with thousands of line items, the inner loop exceeds 2,000.
A third shape: a screen flow with a fault-handler loop that retries failed records. If the retry logic resubmits records without filtering successes, the loop iterates through the original failing set plus any new failures, growing the iteration count on each pass.
A fourth shape: a flow that calls a subflow that calls back to the parent. The iteration counters apply to each loop in each flow, but the cumulative work across flows can still hit other governor limits (CPU, SOQL) before the iteration limit fires.
Why the limit is exactly 2,000
The number was chosen to balance utility with safety. A 2,000-iteration loop with one DML per iteration consumes the full DML budget of 150 within the first 75 iterations, so most production flows hit DML limits long before they approach 2,000 iterations. The iteration limit catches edge cases where each iteration is cheap but the loop is large.
For flows that perform aggregation or filtering, 2,000 is enough to handle typical bulk-trigger contexts. For flows that walk full data sets, 2,000 is intentionally too small; the platform pushes you to batch.
The fix, three paths
Move the per-record work to a record-triggered flow. The cleanest fix for scheduled flows that walk large collections. Instead of scanning every record on a schedule, react to changes as they happen.
For the subscription example, create a record-triggered flow on Subscription with the entry condition "Renewal_Date - TODAY() = 30". The flow fires once per Subscription whose renewal is exactly 30 days away. The platform handles the volume by firing the flow per-record, distributing the work across normal operational time.
The shift requires thinking about the trigger condition. "Subscription was inserted or updated and its renewal is within 30 days" produces redundant fires; "Renewal_Date - TODAY() = 30 was just crossed today" is more precise.
Chunk the work into batches with a Scheduled Path or Apex. When the work must be batched, drive it from Apex.
public class SubscriptionRenewalNoticeBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Renewal_Date__c, Renewal_Notice_Sent__c ' +
'FROM Subscription__c ' +
'WHERE Status__c = \'Active\' AND Renewal_Notice_Sent__c = false'
);
}
public void execute(Database.BatchableContext bc, List<Subscription__c> scope) {
List<Subscription__c> toUpdate = new List<Subscription__c>();
Date threshold = Date.today().addDays(30);
for (Subscription__c s : scope) {
if (s.Renewal_Date__c != null && s.Renewal_Date__c <= threshold) {
s.Renewal_Notice_Sent__c = true;
toUpdate.add(s);
}
}
if (!toUpdate.isEmpty()) {
update toUpdate;
}
}
public void finish(Database.BatchableContext bc) {}
}
The batch processes 200 records per chunk. With 8,500 records, the job runs 43 chunks. No iteration ever exceeds the per-chunk size.
Split the flow into sequential subflows with smaller scopes. When you must stay within flow tooling, split the input by category. A flow that processes subscriptions can branch on a partition key (region, product line, customer tier) and feed each partition through a separate loop.
Each loop now processes a subset that fits under 2,000. The trade-off is that you have to maintain the partition logic and ensure the partitions are balanced.
The fixed example
A record-triggered flow on Subscription with the entry condition:
Conditions:
Renewal_Date__c equals TODAY()+30 (formula)
OR
(Renewal_Date__c equals TODAY()+7 AND Renewal_Notice_Sent_7Day__c equals false)
The flow:
- Get Renewal Owner: query for the Subscription's Account Owner (one record).
- Decision: which notice type?
- Send Email Action: send the appropriate template.
- Update Records: set the renewal-notice flag to true.
The flow runs once per Subscription whose renewal date is exactly 30 days or 7 days away. No loop element is needed because the platform fires the flow per-record. The 2,000-iteration limit does not apply to the per-record invocations.
The flow's design becomes simpler than the scheduled equivalent because the platform handles distribution. Each Subscription's notice runs in its own transaction, with its own governor budget.
For cases where a scheduled approach is genuinely required (an aggregation or report that needs a snapshot view), the batch class above replaces the flow.
Edge case: nested loops
A flow can contain a loop inside another loop. Each loop has its own 2,000-iteration limit. Two nested loops iterating 1,500 and 50 respectively are fine; the inner runs at most 1,500 * 50 = 75,000 total executions, but each loop element advances 1,500 and 50 times respectively.
The catch is the CPU and SOQL limits, which apply per transaction across all loops. Two nested loops with one SOQL each consume a query for every inner iteration. If the inner loop runs 50 times for each of 1,500 outer iterations, the flow fires 75,000 SOQL queries and hits the 100-per-transaction limit long before any loop's iteration limit.
The fix is to bulkify the inner work. Collect all the keys you need before entering the outer loop, run one Get Records that filters by all keys at once, and look up values from the collection inside the loop.
Edge case: loops over collection variables vs Get Records collections
A flow can loop over a collection variable that you populated manually with Add Element actions, or over the result of a Get Records element. The 2,000-iteration limit applies in both cases.
Some flows accumulate items into a collection across multiple branches, then loop over the result. The size of the accumulated collection determines whether the loop fits. Tracking the size with a count variable and short-circuiting before the loop catches the issue before it fires.
Edge case: loops that call Apex actions
A flow loop that invokes an Apex action per iteration multiplies the work. The Apex action runs in the flow's transaction context, consuming the flow's governor budget. If the Apex performs SOQL or DML, the per-iteration cost compounds.
The fix is usually to convert the Apex action to accept a list. A bulkified Apex action processes the entire collection in one call, removing the per-iteration loop entirely. The Invocable Method pattern in Apex is designed for this:
public class RenewalNoticeAction {
@InvocableMethod(label='Send Renewal Notices' callout=true)
public static void send(List<Id> subscriptionIds) {
// Process all ids in one transaction
}
}
The flow now calls RenewalNoticeAction.send once with the full list, instead of looping and calling it per record.
Edge case: scheduled paths within record-triggered flows
A record-triggered flow can include scheduled paths that defer work to a later time. The deferred work runs in its own transaction with its own governor budget. Scheduled paths are useful for time-spaced reminders, follow-ups, and renewal notices.
Trigger: Subscription record created or updated
Path 1 (immediate): mark as in-flight
Path 2 (scheduled, 30 days before Renewal_Date): send 30-day notice
Path 3 (scheduled, 7 days before Renewal_Date): send 7-day notice
The scheduled paths fire on their schedule, one per record. The 2,000-iteration limit never enters the picture because there is no loop.
Test patterns
Flow tests should cover the data-volume scenarios that approach the limit. A unit test fixture creates 2,001 records and exercises the flow path to confirm it handles the limit gracefully or splits the work.
For Apex batch classes replacing flows, a test that asserts the batch processes a representative volume:
@IsTest
static void batchProcessesManySubscriptions() {
List<Subscription__c> subs = new List<Subscription__c>();
for (Integer i = 0; i < 500; i++) {
subs.add(new Subscription__c(
Name = 'Sub ' + i,
Status__c = 'Active',
Renewal_Date__c = Date.today().addDays(30)
));
}
insert subs;
Test.startTest();
Database.executeBatch(new SubscriptionRenewalNoticeBatch(), 200);
Test.stopTest();
Integer notified = [SELECT COUNT() FROM Subscription__c WHERE Renewal_Notice_Sent__c = true];
System.assertEquals(500, notified);
}
The batch processes the 500 records in three chunks. The test confirms all are marked notified.
Diagnosing in production
When the limit fires:
- Identify the flow that threw and the specific loop element.
- Determine the input collection size in the failing run (from flow debug logs).
- Assess whether the flow can be replaced with a record-triggered approach.
- If not, plan a Batch Apex equivalent.
- Migrate, test, and deploy.
The flow debug log shows the loop's iteration count up to the failure point. Knowing the actual count helps decide between record-triggered (if the count is naturally bounded per record) and batch (if the count grows with org data volume).
Anti-pattern: catching the exception in flow fault paths
A fault path that catches the iteration-limit error and resumes the loop is not possible; the runtime halts the flow entirely. Even if the fault path could catch it, restarting the same loop from where it stopped is not supported.
The right pattern is to prevent the limit from being reached, not to recover after.
Defensive habits
Prefer record-triggered flows over scheduled flows that scan collections. The platform handles volume by firing per-record, so the per-flow iteration limit rarely applies.
When a scheduled flow is genuinely needed, plan for the volume the flow will see in three years, not the volume today. A flow that runs against 1,500 records today and approaches 2,000 in a year fails the moment it crosses the line, often during the busiest month of the year.
For complex per-record logic, build Batch Apex. The batch infrastructure is mature, well-monitored, and scales to millions of records without limit concerns.
Test flows with realistic volume. The default sandbox often has fewer than 100 records of the type the flow processes; production may have tens of thousands. A volume test in a partial-copy sandbox surfaces issues before they reach production.
Document the iteration-limit risk for any flow that loops over a record collection. The risk should be a known property of the design, not a surprise discovered during an incident.
Quick recovery checklist
- Identify the failing flow and the loop element.
- Determine the input volume that triggered the limit.
- Choose: record-triggered, batched scheduled paths, or Batch Apex.
- Migrate the logic.
- Deactivate the old flow once the replacement is tested.
- Monitor the first scheduled run after migration.
Flow iteration limits are an architectural signal. Each incident is an opportunity to move work to a more appropriate paradigm. Treating the limit as a forcing function for better design pays off in fewer incidents and more maintainable automation.
Further reading from Salesforce
- Salesforce Help: Flow Limits and Considerations
- Salesforce Help: Choose the Right Flow Type
- Apex Developer Guide: Using Batch Apex
- Architect: Automation Decision Guide
- Trailhead: Build Flows with Flow Builder
Related dictionary terms
Share this fix
Related Flow errors
An unhandled fault has occurred in this flow
FlowA Flow ran into an error somewhere mid-run and had nowhere to go. By default Salesforce sends the unhandled-fault email to the running user …
Couldn't find the subflow with name <X> or no active version
FlowThe parent flow tries to invoke a subflow that either doesn't exist in this org or has no active version. Subflows must be deployed AND acti…
Flow: changes from this flow won't be saved because of the Save Order of Execution
FlowA before-save record-triggered flow tried to do something only after-save can do (update related records, make callouts) — or an after-save …
The Apex action <name> couldn't be invoked because it's not @InvocableMethod or has the wrong signature
FlowA Flow tried to call an Apex method as an action, but the method either lacks the `@InvocableMethod` annotation, doesn't return / accept a `…
The flow failed to access the value for {!variable} because it hasn't been set or assigned
FlowA flow tried to read a variable that no element ever wrote to. Either an upstream Get Records returned nothing (so the result variable staye…