No. Salesforce blocks future calls from any of a Batch class’s methods (start, execute, finish) with:
System.AsyncException: Future method cannot be called from a future or batch method.
The reason is fan-out control: a batch processing millions of records in thousands of chunks could enqueue thousands of future invocations in seconds, exhausting async capacity. Salesforce simply forbids it.
What you’d write that won’t work
public class MyBatch implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator('SELECT Id FROM Account');
}
public void execute(Database.BatchableContext ctx, List<Account> scope) {
List<Id> ids = new List<Id>();
for (Account a : scope) ids.add(a.Id);
MyService.notifyAsync(ids); // AsyncException — chunk fails
}
public void finish(Database.BatchableContext ctx) {}
}
public class MyService {
@future
public static void notifyAsync(List<Id> ids) { }
}
Each chunk throws, NumberOfErrors climbs, no records updated.
The fix: use Queueable
Queueable doesn’t have the same restriction. You can enqueue Queueable jobs from anywhere in batch — start, execute, or finish:
public class MyBatch implements Database.Batchable<sObject> {
public void execute(Database.BatchableContext ctx, List<Account> scope) {
List<Id> ids = new List<Id>();
for (Account a : scope) ids.add(a.Id);
System.enqueueJob(new MyServiceQueueable(ids)); // allowed
}
public void finish(Database.BatchableContext ctx) {}
}
public class MyServiceQueueable implements Queueable {
private List<Id> ids;
public MyServiceQueueable(List<Id> ids) { this.ids = ids; }
public void execute(QueueableContext ctx) { /* ... */ }
}
Or: just do the work inside execute
If the only reason you wanted a future call was to “make this async,” batch is already async. You can do the work directly in execute. If the work is a callout, add Database.AllowsCallouts:
public class MyBatch implements
Database.Batchable<sObject>,
Database.AllowsCallouts
{
public void execute(Database.BatchableContext ctx, List<Account> scope) {
for (Account a : scope) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ERP/accounts/' + a.Id);
req.setMethod('PUT');
new Http().send(req);
}
}
}
Most “I need future-from-batch” scenarios reduce to “I just need callouts” — and the answer is Database.AllowsCallouts, not a future method.
When the future-from-batch error appears
| From | Calls @future | Result |
|---|---|---|
start | Yes | AsyncException |
execute | Yes | AsyncException |
finish | Yes | AsyncException |
finish calls Queueable | — | Allowed |
execute enqueues Queueable | — | Allowed (one per chunk) |
Common interview follow-ups
- Can
finishcall out directly? — Yes, withDatabase.AllowsCallouts. - Can I work around with
System.runAs? — No. The rule is about job context, not user context. - Why is Queueable allowed? — Different API generation. Queueable was designed with chaining/fan-out controls built in.
Verified against: Apex Developer Guide — Batch Apex Limitations. Last reviewed 2026-05-17.