@future method called inside a loop

ID

apex.correctness.future_called_in_loop

Severity

high

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Resource Management

Language

Apex

Description

Reports a call inside a for, while or do loop to a @future method declared in the same class, whether called plainly or through a qualified reference. Only same-class calls are reported: OpenGrep does not resolve a callee across files, so a loop calling a @future method declared in another class is not detected.

Rationale

A transaction may queue 50 asynchronous calls, and each @future invocation spends one. A loop over a trigger’s 200 records therefore throws LimitException on the fifty-first record, and because triggers run in batches of 200 the failure appears only once the data is large enough - typically in production, on an import, and never in the test that ran with three records. Every queued call is also a separate transaction with its own governor limits and its own start-up cost, so even below the ceiling the work is far slower than one call would be.

The following code illustrates the pattern detected by this rule:

public class AccountSyncService {

    @future
    public static void syncOne(Id accountId) {
        System.debug(LoggingLevel.INFO, accountId);
    }

    @future(callout=true)
    public static void pushOne(Id accountId) {
        System.debug(LoggingLevel.INFO, accountId);
    }

    @Future
    public static void auditOne(Id accountId) {
        System.debug(LoggingLevel.INFO, accountId);
    }

    public static void syncAll(List<Account> accounts) {
        // FLAGGED: @future method called inside a loop
        for (Account a : accounts) {
            syncOne(a.Id);
        }
    }

    public static void pushAll(List<Id> accountIds) {
        Integer n = 0;
        // FLAGGED: @future method called inside a loop
        while (n < accountIds.size()) {
            pushOne(accountIds[n]);
            n++;
        }
    }

    public static void auditAll(List<Id> accountIds) {
        // FLAGGED: @future method called inside a loop
        for (Integer i = 0; i < accountIds.size(); i++) {
            AccountSyncService.auditOne(accountIds[i]);
        }
    }

    public static void syncAllBulk(List<Account> accounts) {
        Set<Id> ids = new Set<Id>();
        for (Account a : accounts) {
            ids.add(a.Id);
        }
        syncMany(ids);
    }

    @future
    public static void syncMany(Set<Id> accountIds) {
        System.debug(LoggingLevel.INFO, accountIds.size());
    }

    public static void stampAll(List<Account> accounts) {
        for (Account a : accounts) {
            stampOne(a);
        }
        update accounts;
    }

    public static void stampOne(Account a) {
        a.Description = 'stamped';
    }
}

Remediation

Collect the ids in the loop and call the @future method once, after it, with the whole collection: a @future method may take a Set<Id> or a List<Id> and iterate inside the asynchronous transaction. Where the work needs more than one asynchronous transaction, a Queueable chain or a Database.Batchable class gives you a supported way to span several.

Configuration

This detector does not need any configuration.

© 2026 Xygeni. All rights reserved