Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

The quick version: the limit is 50 @future calls per transaction, and you have tried to make the 51st. Verified in a scratch org on Summer ’26: Limits.getLimitFutureCalls() returns 50. The cause is almost always a future call inside a loop, or a future call in a trigger that received 200 records.

The trigger case, which is nearly all of them

trigger AccountTrigger on Account (after update) {
    for (Account a : Trigger.new) {
        AccountSync.pushToErp(a.Id);   // @future — one call per record
    }
}

A Data Loader update of 200 accounts fires 200 future calls in one transaction, and the 51st throws. The fix is the same as every other bulkification fix: pass the whole collection once.

AccountSync.pushToErp(Trigger.newMap.keySet());

@future(callout=true)
public static void pushToErp(Set<Id> accountIds) { ... }

One call, one set of IDs, and the future method loops internally with its own fresh limits.

The limit is per transaction, not per method

Fifty across everything the transaction does. A trigger making two future calls per record hits it at 25 records, not 25 per method. And if a flow, a process and a trigger on the same object each make one, the count is the sum, which is why the error sometimes appears after a change that looked unrelated.

Future calls made from a batch execute get a fresh count each chunk, because each chunk is its own transaction. That is genuinely useful and it is also how a batch quietly makes thousands of future calls.

What future cannot do, and why that matters here

@future takes only primitives, collections of primitives, and collections of those. No sObjects, no custom types. That restriction pushes people toward passing one ID at a time, which is exactly the pattern that causes this error.

A future method also returns nothing, cannot be chained, cannot be monitored except through the Apex Jobs page, and cannot be called from another future method or from a batch start. Queueable has none of those limitations: it takes objects, returns a job ID you can query, and can chain. The decision between them is in choosing an async mechanism, and for new code the honest answer is that Queueable is rarely worse.

Queueable has the same 50

Do not migrate to Queueable expecting a bigger allowance. Limits.getLimitQueueableJobs() also returns 50 in a synchronous transaction. What Queueable gives you is the ability to pass a collection sensibly and to chain one job from another, both of which reduce the number of jobs you need. The limit is the same. The design around it is better.

Checking before you call

In code that might run in an unpredictable context, ask rather than assume:

if (!System.isFuture() && !System.isBatch()
    && Limits.getFutureCalls() < Limits.getLimitFutureCalls()) {
    AccountSync.pushToErp(ids);
}

The context checks matter as much as the count: calling a future method from inside a future method throws a different error, and it is a real trap in shared utility code.

Frequently asked

Is there a daily limit too? Yes. Asynchronous Apex executions per 24 hours are limited to 250,000 or 200 times your user licence count, whichever is higher, shared across future, queueable, batch and scheduled.

Does the limit differ in async context? The per-transaction count is 50 either way, but a future method cannot call another future method at all, so the practical answer there is zero.

How do I see what is queued? Setup, Apex Jobs, or query AsyncApexJob. Future calls show with a JobType of Future and no batch information.