Table of Contents
One of the most common architectural decisions Salesforce developers face is: should I use Batch Apex or Queueable Apex? Both process work asynchronously, both have their own governor limits, and both are appropriate in different situations. Choosing the wrong one leads to avoidable complexity and maintenance headaches.
Quick Decision Framework
Before diving into the details, here is the fast answer:
- Use Batch Apex when you need to process a large volume of records (thousands to millions), need scope control, need status tracking, or need to run on a schedule.
- Use Queueable Apex when you need to offload a smaller amount of work from a trigger or synchronous context, need to chain jobs, need to make callouts, or need to pass complex objects between jobs.
What Is Batch Apex?
Batch Apex is designed for processing large volumes of records. It implements the Database.Batchable interface with three methods:
- start(): Returns a query locator or iterable that defines which records to process
- execute(): Called once per batch (scope), processes one chunk of records. Default scope is 200 records per chunk.
- finish(): Called once after all batches complete. Good for sending completion emails or chaining the next job.
public class UpdateAccountStatusBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, AnnualRevenue FROM Account WHERE Rating = null'
);
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
for (Account acc : scope) {
acc.Rating = acc.AnnualRevenue > 1000000 ? 'Hot' : 'Warm';
}
update scope;
}
public void finish(Database.BatchableContext bc) {
System.debug('Batch complete: ' + bc.getJobId());
}
}
// Execute:
Database.executeBatch(new UpdateAccountStatusBatch(), 200);Key characteristics of Batch Apex:
- Can process up to 50 million records via QueryLocator (2,000 via Iterable)
- Each execute() call gets a fresh set of governor limits
- Only 5 active batch jobs per org at a time (total across all users)
- Can be scheduled using
System.scheduleBatch() - Job status visible in Setup → Apex Jobs
What Is Queueable Apex?
Queueable Apex implements the Queueable interface with a single method: execute(QueueableContext context). It was introduced to overcome the limitations of @future methods — specifically to support callouts, job chaining, and passing complex objects.
public class SendWelcomeEmailJob implements Queueable {
private List<Id> userIds;
public SendWelcomeEmailJob(List<Id> userIds) {
this.userIds = userIds;
}
public void execute(QueueableContext context) {
List<User> users = [SELECT Id, Email, Name FROM User WHERE Id IN :userIds];
// ... send welcome emails
}
}
// Enqueue from a trigger:
System.enqueueJob(new SendWelcomeEmailJob(userIds));Key characteristics of Queueable Apex:
- Can chain jobs (enqueue another Queueable from inside execute())
- Supports callouts to external services when combined with
Database.AllowsCallouts - Can hold references to complex objects (unlike @future which only accepts primitives)
- Up to 50 jobs enqueued per transaction in a non-test context
- Maximum chain depth of 5 in production (1 in Developer Edition orgs)
- Returns a job ID for monitoring
Side-by-Side Comparison
- Record volume: Batch = millions of records; Queueable = smaller targeted set
- Governor limits: Batch gets fresh limits per chunk; Queueable has single execution limits
- Callouts: Both require Database.AllowsCallouts mixin
- Scheduling: Batch can be scheduled; Queueable must be triggered programmatically
- Chaining: Batch chains via finish(); Queueable chains via System.enqueueJob() in execute()
- Status tracking: Batch has built-in job status UI; Queueable returns a job ID only
- Complexity of passing data: Batch data comes from SOQL; Queueable can receive any Apex objects
Chaining Jobs
Both mechanisms support chaining, but with important differences.
Batch chaining — chain in the finish() method:
public void finish(Database.BatchableContext bc) {
Database.executeBatch(new NextBatchJob(), 200);
}Queueable chaining — enqueue a new job inside execute():
public void execute(QueueableContext context) {
// ... do work ...
if (hasMoreWork) {
System.enqueueJob(new NextQueueableJob(remainingIds));
}
}Error Handling
Batch Apex has more built-in error isolation — if one batch chunk fails, the other chunks continue processing. The failure is recorded in the AsyncApexJob record. Queueable Apex, by contrast, fails the entire job if an exception is thrown and not caught.
Best practice for Queueable error handling:
public void execute(QueueableContext context) {
try {
processWork();
} catch (Exception e) {
insert new Error_Log__c(
Message__c = e.getMessage(),
Stack_Trace__c = e.getStackTraceString()
);
}
}Real-World Examples
Use Batch Apex for:
- Nightly cleanup jobs that update stale records (millions of records, scheduled)
- Data migration during a Salesforce deployment
- Recalculating rollup summaries across all Account records
- Sending monthly statements to all active customers
Use Queueable Apex for:
- Making a REST API callout after a trigger fires (trigger can't make synchronous callouts)
- Processing a set of IDs from a trigger that involves complex non-record data
- Multi-step async workflows where each step depends on the previous (job chaining)
- Offloading a CPU-intensive calculation from a synchronous context
Governor Limits Reference
- Batch — active batch jobs per org: 5
- Batch — max records via QueryLocator: 50,000,000
- Batch — SOQL per execute() call: 200
- Batch — DML per execute() call: 150
- Queueable — jobs per transaction (sync): 50
- Queueable — max chain depth (production): 5
- Queueable — SOQL per execution: 200
- Future — calls per transaction: 50