Future vs Queueable vs Batch Apex: Which Async Apex to Use

Hey friends,

Salesforce gives you three main ways to run Apex later instead of right now: future methods, Queueable Apex and Batch Apex. (Scheduled Apex is the fourth, and it decides when work starts rather than how.) They overlap enough to be confusing, and Platform Developer II loves to ask which one fits a scenario. Here is what each does, what it can’t do, and a quick way to choose.

Why go async at all

Async Apex runs in its own transaction when resources are free, with higher governor limits than synchronous code: 200 SOQL queries instead of 100, 25 MB heap instead of 10 MB, and 60 seconds of CPU time instead of 10. You reach for it for three reasons: callouts from a trigger (not allowed synchronously), work that’s too heavy for one transaction, and work the user shouldn’t wait on.

Decision chart: more than about 50,000 records or needs to process every record in a query, use Batch Apex; needs sObject or object parameters, a job ID, or chaining, use Queueable; otherwise a simple fire-and-forget call with primitive parameters can use a future method. Scheduled Apex sets when any of them starts
Diagram: choosing between future methods, Queueable Apex and Batch Apex.

Future methods: the simplest, and the most limited

A future method is a static method marked @future. Calling it queues it and returns immediately.

AccountSync.apex
1public class AccountSync {
2    @future(callout=true)
3    public static void pushToErp(Set<Id> accountIds) {
4        List<Account> accts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
5        // build an HttpRequest and send it for each account
6    }
7}
  • Must be static and return void.
  • Parameters must be primitives, or arrays and collections of primitives. You can’t pass sObjects, so pass IDs and query inside, which also gets you current data.
  • No job ID is returned, so you can’t monitor it from code.
  • A future method can’t call another future method, so you can’t chain them.
  • Up to 50 future calls per transaction. Add callout=true to make callouts.

Queueable Apex: the default choice now

Queueable Apex is a class that implements Queueable. You enqueue an instance, so it can hold any member variables you like, sObjects and custom types included.

CreateFollowUps.apex
1public class CreateFollowUps implements Queueable, Database.AllowsCallouts {
2    private List<Opportunity> opps;
3    public CreateFollowUps(List<Opportunity> opps) {
4        this.opps = opps;
5    }
6    public void execute(QueueableContext ctx) {
7        List<Task> tasks = new List<Task>();
8        for (Opportunity o : opps) {
9            tasks.add(new Task(WhatId = o.Id, Subject = 'Follow up'));
10        }
11        insert tasks;
12        // chain the next step: one child job per execution
13        System.enqueueJob(new NotifyOwners(opps));
14    }
15}
Anonymous.apex
1Id jobId = System.enqueueJob(new CreateFollowUps(oppList));
2AsyncApexJob job = [SELECT Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];
  • Non-primitive state: pass sObjects or your own classes through the constructor.
  • Job ID: System.enqueueJob returns the AsyncApexJob ID, so you can query its status.
  • Chaining: a running Queueable can enqueue one child job. Use it for step-by-step work.
  • Up to 50 jobs enqueued per synchronous transaction; implement Database.AllowsCallouts for callouts.

Anything you’d write as a future method can be written as a Queueable, with monitoring and chaining on top. That’s why most new code uses Queueable.

Batch Apex: for big data volumes

Batch Apex implements Database.Batchable with three methods. start returns the records (a Database.QueryLocator can return up to 50 million), execute runs once per chunk, and finish runs once at the end.

CloseStaleOpps.apex
1public class CloseStaleOpps implements Database.Batchable<SObject>, Database.Stateful {
2    private Integer closed = 0;   // kept across chunks because of Stateful
3    public Database.QueryLocator start(Database.BatchableContext bc) {
4        return Database.getQueryLocator(
5            'SELECT Id FROM Opportunity WHERE IsClosed = false AND CloseDate < LAST_N_DAYS:180');
6    }
7    public void execute(Database.BatchableContext bc, List<Opportunity> scope) {
8        for (Opportunity o : scope) {
9            o.StageName = 'Closed Lost';
10        }
11        update scope;
12        closed += scope.size();
13    }
14    public void finish(Database.BatchableContext bc) {
15        System.debug('Closed ' + closed + ' opportunities');
16    }
17}
Anonymous.apex
1Id jobId = Database.executeBatch(new CloseStaleOpps(), 200);
  • Each execute call is its own transaction with fresh governor limits. A failed chunk rolls back alone; the others keep their changes.
  • Default scope is 200 records per chunk; you can set 1 to 2,000.
  • Member variables reset between chunks unless the class implements Database.Stateful.
  • Up to 5 batch jobs can be queued or active at once, with up to 100 more waiting in the Apex flex queue.
  • Chain batches by calling Database.executeBatch from finish.

Cert Lab by Nick Frates

Practise the async scenarios

The Platform Developer II Cert Lab ($9.99) drills async Apex, trigger frameworks and governor limits with scenario questions, and explains why each wrong option is wrong.

See the Platform Developer II Cert Lab – $9.99 →

Side by side

FutureQueueableBatch
How you start itCall the @future methodSystem.enqueueJobDatabase.executeBatch
ParametersPrimitives onlyAny type (via class state)Any type (via class state)
Returns a job IDNoYesYes
ChainingNoOne child job per executionFrom finish()
VolumeSmallSmall to mediumUp to 50 million records
LimitsOne async transactionOne async transaction per jobFresh limits per chunk
Calloutscallout=trueDatabase.AllowsCalloutsDatabase.AllowsCallouts

Testing async code

Async jobs don’t run in a test until you call Test.stopTest(). Everything queued after Test.startTest() runs synchronously at stopTest, so you can assert on the results right after it.

CloseStaleOppsTest.apex
1@IsTest
2static void closesStaleOpps() {
3    // insert test opportunities with an old CloseDate here
4    Test.startTest();
5    Database.executeBatch(new CloseStaleOpps());
6    Test.stopTest();   // the batch finishes here
7    System.assertEquals(0, [SELECT COUNT() FROM Opportunity WHERE IsClosed = false]);
8}

In a test, a batch class runs only one execute call, so keep test data to 200 records or fewer.

Quick way to pick

  • Processing every record in a large query, or more than one transaction can hold: Batch.
  • Need sObjects as input, a job ID, or step-by-step chaining: Queueable.
  • Tiny fire-and-forget call with IDs, in older code you’re maintaining: future is fine, though Queueable does it too.
  • Need it to run nightly or on a set time: Scheduled Apex that starts one of the three.

FAQ

Can I call a future method from a batch job?

No. Future methods can’t be called from a batch job or from another future method. Use a Queueable instead.

Can a Queueable start a batch job?

Yes. Database.executeBatch can be called from a Queueable’s execute method.

How many records can Batch Apex process?

A Database.QueryLocator in start() can return up to 50 million records. An Iterable start() is held to the normal query row limit.

Do async jobs share governor limits with the code that queued them?

No. Each job runs in its own transaction with the async limits, and each batch execute call gets its own fresh limits.

Hope this helps!

Best,

Nick

Keep reading

See all posts →

Salesforce Saturdays

Join 1,033 Salesforce pros getting one 10-minute tip on Salesforce tech or career growth every Saturday.