System.LimitException: Too many future calls: 51
What does this error mean?
Salesforce allows a maximum of 50 @future method calls per transaction. Each call to a method annotated with @future increments this counter regardless of which class or method is called. When the 51st call is attempted, Salesforce throws this uncatchable exception and rolls back the transaction.
This limit exists because @future jobs are queued resources. Enqueuing 50+ jobs from a single transaction can flood the async queue and starve other orgs in the multitenant environment.
Common Causes
1. @future call inside a trigger loop
Calling a @future method once per record in a trigger is the most common pattern. With bulk loads of 51+ records, you immediately exceed the limit.
2. Multiple trigger handlers each calling @future
Different trigger handlers on the same object each calling separate @future methods — the total across all of them counts against one limit per transaction.
How to Fix It
Solution 1: Collect IDs and call @future once
Pass a Set<Id> to a single @future call instead of calling it once per record.
// ❌ BAD — @future per record
for (Account acc : Trigger.new) {{
MyFutureClass.doWork(acc.Id); // 51 records = error
}}
// ✅ GOOD — single @future with all IDs
Set<Id> ids = Trigger.newMap.keySet();
MyFutureClass.doWork(ids); // one call, any size
// The @future method handles the set
@future
public static void doWork(Set<Id> accountIds) {{
List<Account> accs = [SELECT Id FROM Account WHERE Id IN :accountIds];
// process...
}}
Solution 2: Switch to Queueable Apex
Queueable Apex is the modern replacement for @future. It supports chaining, accepts non-primitive parameters, and has the same limit of 50 enqueues per transaction — but with much better flexibility.
public class AccountQueueable implements Queueable {{
private Set<Id> accountIds;
public AccountQueueable(Set<Id> ids) {{
this.accountIds = ids;
}}
public void execute(QueueableContext ctx) {{
// Process all accounts in one async execution
List<Account> accs = [SELECT Id FROM Account WHERE Id IN :accountIds];
}}
}}
// Enqueue once from trigger
System.enqueueJob(new AccountQueueable(Trigger.newMap.keySet()));
Pro Tip: Use Limits.getFutureCalls() to check how many @future calls have been made in the current transaction. If you have multiple handlers, centralise all async dispatching in one place to avoid accidentally compounding calls.