Queueable vs Batch Apex vs Future: When to Use What
"Should I use Queueable, Batch Apex, or Future?"
This is one of the most common questions every Salesforce developer asks at some point. While all three are used for asynchronous processing, they serve different use cases, limits, and patterns.
We’ll break down when to use each async tool, with real code examples, performance comparisons, and gotchas I’ve learned from years in the trenches.
Why Use Asynchronous Apex?
Asynchronous Apex allows you to run long-running operations in the background, freeing up system resources and improving user experience.
Common use cases:
- Processing thousands of records
- Calling external services
- Sending bulk emails
- Updating related data in complex logic
Why Async Matters in Salesforce
If you've ever hit a "Too many SOQL queries" or "CPU time limit exceeded" error, you already know why asynchronous processing is a big deal. Salesforce enforces strict governor limits to keep shared resources stable, but that also means you can't always get everything done in a single transaction.
That's where @future
, Queueable, and Batch Apex come in. They're all tools for handling logic after the current transaction finishes?-?but they serve very different purposes.
Quick Comparison: What’s the Difference?
Feature | @future | Queueable Apex | Batch Apex |
---|---|---|---|
Governor Limits | Same as caller | Higher limits | Highest limits |
Chaining | No | Yes | No |
Statefulness | Stateless | Stateful | Stateful |
Record Scope | 1 ID/record | 1 ID/record | 50K-50M records |
Best For | Quick async ops | Complex async chains | Large data volumes |
1. @future
: The Simple (But Limited) Option
Before Queueable Apex existed, @future
was the go-to for async work. It still has its place, but it's limited.
When to Use:
- Simple, one-off async tasks (e.g., logging, basic callouts)
- Maintaining older code where refactoring isn't feasible
Example: Sending a Callout After Account Update
public class AccountService {
public static void updateAccount(Id accountId) {
// Immediate DML
Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];
acc.Name += ' (Updated)';
update acc;
// Async callout
makeCallout(acc.Id);
}
@future(callout=true)
public static void makeCallout(Id accountId) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/update');
req.setMethod('POST');
new Http().send(req); // Callout won't block main transaction
}
}
Output:
- Sync: Account name updates instantly
- Async: Callout happens later (won't fail if DML takes too long)
Limitations:
- No chaining (can't call another
@future
from it) - Future Methods can only return void type
- Only accepts primitive parameters (no sObjects or lists)
- Harder to debug?-?not visible in the Apex Jobs UI
2. Queueable Apex — Future’s Smarter Cousin
Queueable is like Future++ — it gives you more control, supports chaining, and can pass complex objects.
When to Use:
- Post-trigger logic (e.g., enrich records, send emails, kick off follow-up jobs)
- Chained workflows (e.g., update ---> callout ---> notify ---> status update)
- Complex data passing (lists, sObjects, custom classes)
- Anything that used to be a
@future
but needs more flexibility
Example: Multi-Step Account Processing
public class AccountEnricher implements Queueable, Database.AllowsCallouts {
private List accountIds;
// Constructor preserves state
public AccountEnricher(List accountIds) {
this.accountIds = accountIds;
}
public void execute(QueueableContext ctx) {
// Step 1: Enrich account data
List accounts = [SELECT Id, Website FROM Account WHERE Id IN :accountIds];
for (Account acc : accounts) {
acc.Industry = inferIndustry(acc.Website);
}
update accounts;
// Step 2: Chain a follow-up job
if (!accounts.isEmpty()) {
System.enqueueJob(new ERPNotifier(accountIds)); // One chain allowed
}
}
private String inferIndustry(String website) {
if (website == null) return 'Other';
if (website.contains('.edu')) return 'Education';
if (website.contains('.tech')) return 'Technology';
return 'Other';
}
}
How to Call It From a Trigger Handler:
Never heard of a trigger handler? Here’s my guide on how to set one up.
Trigger Handler Integration:
public class AccountTriggerHandler {
public static void handleAfterInsert(List newAccounts) {
List accountIds = new List();
for (Account acc : newAccounts) {
if (acc.Website != null) {
accountIds.add(acc.Id);
}
}
if (!accountIds.isEmpty()) {
System.enqueueJob(new AccountEnricher(accountIds));
}
}
}
Then, in your trigger:
trigger AccountTrigger on Account (after insert) {
AccountTriggerHandler.handleAfterInsert(Trigger.new);
}
Limitations:
- Job enqueuing limits:
- Chaining jobs:
In a synchronous context (like a trigger or controller), you can enqueue up to 50 Queueable jobs per transaction.
In an asynchronous context (like another Queueable or Batch job), you can enqueue only one job per transaction.
You can chain Queueable jobs (job ---> job ---> job…), but each job can only enqueue one child.
There’s no hard system limit on the number of chained jobs — except in Developer Edition orgs, where you’re capped at a max chain depth of 5 jobs total.
Key Advantages Over @future
:
- Preserves state (instance variables)
- Chainable workflows (one job can enqueue another)
- Visible in Apex Jobs UI
- Accepts complex parameters
Warning: You can only chain one job per execution in async contexts.
3. Batch Apex – The Heavyweight Champion
Batch Apex is made for large datasets — up to 50 million records. It’s your go-to tool for nightly jobs, data cleanup, and more.
When to Use:
- Processing 50K+ records (e.g., data migrations)
- Scheduled operations (e.g., nightly syncs, score recalculations)
- High-volume updates or deletes
Each execute
method gets its own transaction, so governor limits are reset per chunk — but that also means it's slower and more transactional than Queueable.
Example: Nightly Account Cleanup
global class AccountCleanupBatch implements Database.Batchable {
global Database.QueryLocator start(Database.BatchableContext bc) {
// Query all accounts needing cleanup
return Database.getQueryLocator(
'SELECT Id, LastActivityDate FROM Account WHERE LastActivityDate < LAST_N_DAYS:90'
);
}
global void execute(Database.BatchableContext bc, List scope) {
// Delete stale accounts
delete scope;
}
global void finish(Database.BatchableContext bc) {
// Optional: Send email report
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setSubject('Batch Complete');
Messaging.sendEmail(new List{mail});
}
}
Execution:
// Run batch (200 records at a time)
Id batchId = Database.executeBatch(new AccountCleanupBatch(), 200);
Output:
- Processes millions of records in chunks
- Avoids governor limits (separate limits per batch)
Batch-Specific Benefits:
- Governor limits reset per batch
- Handles millions of records
- Built-in scheduling
Performance Tip: Test different batch sizes (start with 200) to balance speed vs. limits.
Real-World Lessons Learned
Lesson 1: @future
Deadlocks
Problem: Calling @future
from a trigger can hit async limits (max 50 per transaction).
Fix: Use Queueable instead—it has higher limits.
Lesson 2: Queueable Chain Depth
Problem: Chaining too many jobs fails in Dev Edition orgs (max 5 chains).
Fix: Track depth in a custom object or setting.
Lesson 3: Batch Timeouts
Problem: Long-running batches fail at 10 minutes per execute().
Fix:
- Reduce batch size
- Optimize SOQL in loops
Real-Life Use Case Comparison
Scenario | Best Tool | Why |
---|---|---|
Send welcome email on contact insert | Future | Lightweight, fire-and-forget |
Update related records in chain | Queueable | Can chain and track |
Clean up 1 million old records | Batch Apex | Handles massive volumes |
Process complex input objects | Queueable | Supports custom types |
Monitor job status in UI | Queueable / Batch | Shown in Apex Jobs tab |
Performance Benchmarks
Test: Process 10,000 Account records
Method | Time | Governor Limit Safety |
---|---|---|
@future | Fail | Poor (max 50 calls/transaction) |
Queueable | 5 min | Good (higher limits) |
Batch (size 200) | 3 min | Best (limits reset per batch) |
Final Thoughts
Salesforce gives us three main tools for async logic — and each one has its place:
- Use
@future
for quick, simple jobs. - Use Queueable for flexibility, chaining, and passing rich data.
- Use Batch Apex when you’re working with huge data volumes or scheduled cleanups.
The best developers don’t just know the syntax — they know when and why to reach for each tool.
Your Turn:
Which async tool saved your project? Share your story below!
(0) Comments