Queueable vs Future vs Batch: When to Use Each Async Pattern
Salesforce gives you three async patterns and choosing the wrong one wastes hours. Here is the decision tree I use on every project. If you need to process more than 50,000 records, use Batch Apex. If you need to chain async jobs (job A finishes, then job B starts), use Queueable. If you just need to make a callout from a trigger and do not care about chaining, use @future.
The nuances matter. @future methods cannot be called from other @future methods or from batch classes. They cannot accept sObjects as parameters (only primitive types and collections of primitives). They have no job ID, so you cannot monitor them. Queueable solves all of these problems: it accepts sObjects, returns a job ID, and can chain to another Queueable. The only downside is you can only chain one Queueable from another Queueable (not two).
Batch Apex is the heavy lifter. It processes records in chunks of up to 2,000 (default 200), each chunk getting fresh governor limits. But it is slow to start (queued behind other batch jobs) and you can only have 5 concurrent batch jobs per org. For anything under 50K records that needs to run immediately, Queueable is almost always the better choice.
Code Example
// DECISION TREE:
// Need callout from trigger, no chaining? → @future(callout=true)
// Need to chain jobs or pass sObjects? → Queueable
// Processing > 50K records? → Batch Apex
// @future — simplest, most limited
@future(callout=true)
public static void syncToExternalSystem(Set<Id> recordIds) {
List<Account> accounts = [SELECT Id, Name FROM Account
WHERE Id IN :recordIds];
// Make HTTP callout here
}
// Queueable — chainable, monitorable
public class AccountSyncQueueable implements Queueable, Database.AllowsCallouts {
private List<Account> accounts;
public AccountSyncQueueable(List<Account> accounts) {
this.accounts = accounts;
}
public void execute(QueueableContext ctx) {
// Process accounts, make callouts
// Chain next job if needed:
if (!moreWork.isEmpty()) {
System.enqueueJob(new FollowUpQueueable(moreWork));
}
}
}
// Usage: System.enqueueJob(new AccountSyncQueueable(accounts));
// Batch — for massive data volumes
public class AccountCleanupBatch implements
Database.Batchable<SObject>, Database.Stateful {
public Integer processedCount = 0;
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator(
'SELECT Id, Name FROM Account WHERE Cleanup_Flag__c = true'
);
}
public void execute(Database.BatchableContext ctx, List<Account> scope) {
// Each batch of 200 gets fresh limits
processedCount += scope.size();
}
public void finish(Database.BatchableContext ctx) {
System.debug('Processed ' + processedCount + ' records');
}
}Need this implemented in your org?
I've shipped these patterns in production for 10+ years.
View Consulting →Enjoyed this? Get more like it.
Glen's Musings — AI, investing, and building things. Occasional. Free.
More Apex Tips
Apex Bulkification: The Only Pattern You Need to Memorize
Every Apex developer learns "don't put SOQL in a loop" on day one. But bulkification goes deeper than that. The real pat...
Read moreBeginnerThe Test Data Factory Pattern That Saves Hours Every Sprint
Most Salesforce projects start with test classes that create their own data inline. By sprint three, you have 40 test cl...
Read moreIntermediateCustom Metadata Types: Runtime Configuration Without Deployments
Custom Settings were the old way to store configuration. Custom Metadata Types are the upgrade, and the difference matte...
Read more