Read the screenplay: FANNIEGATE — $7 trillion. 17 years. The biggest fraud in American capital markets.
ApexAdvanced2026-03-04

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