Future vs Queueable vs Batch vs Schedulable in Salesforce Apex: A Practical Selection Guide
Salesforce provides four main asynchronous processing tools — @future, Queueable, Batch, and Schedulable — each designed for different use cases. Choosing the right one impacts performance, reliability, governor limit management, and the overall user experience.
In this guide, we’ll break down their strengths, ideal use cases, and limitations. You’ll also find a simple selection matrix and real-world code examples so you can decide quickly and build with confidence.
? Selection Matrix — When to Use Each Tool
| Capability / Need | @future | Queueable | Batchable | Schedulable |
|---|---|---|---|---|
| Primary use | Fire-and-forget, lightweight tasks | Async job with parameters & chaining | Large data processing in chunks | Run jobs on a schedule |
| Data volume | Tiny → small | Small → medium | Large (thousands–millions) | Any (often launches Queueable/Batch) |
| Callouts | ✅ via @future(callout=true) |
✅ with Database.AllowsCallouts |
✅ in execute with AllowsCallouts |
✅ if job supports callouts |
| Chaining | ❌ | ✅ (chain one job per execute) | ✅ via finish() (start another Batch/Queueable) |
N/A (but can launch Batch/Queueable) |
| Complex state / objects | ❌ (primitives only) | ✅ (supports complex types and sObjects) | ✅ via Database.Stateful |
❌ (used mainly as a launcher) |
| Error handling | Minimal | Good (within job context) | Strong (per-batch try/catch) | Minimal (acts as orchestrator) |
| Monitoring | AsyncApexJob |
AsyncApexJob |
AsyncApexJob + Apex Flex Queue |
Scheduled Jobs list / AsyncApexJob |
| Governor friendliness | Basic | Better | Excellent for bulk | Depends on downstream job |
| Typical examples | Log touches, quick recalcs | Sync CRM with external API | Rebuild rollups for 2M records | Nightly compliance or cleanup runs |
? Rule of Thumb
-
Big dataset? → Use Batch Apex
-
Need callouts, chaining, or complex params? → Go with Queueable
-
Just a quick, lightweight operation? → Use @future
-
Need to run it on a schedule? → Use Schedulable, often to launch a Batch or Queueable job
? Real-World Scenario
Let’s say you need to run a nightly maintenance job that:
-
Scans a large number of stale Cases and updates their status → Batch
-
Notifies an external incident management system for escalated Cases → Queueable
-
Logs a simple “touched” marker for telemetry → @future
-
Runs every night at 1:00 AM → Schedulable
Here’s how you can design that workflow step by step.
? A) Schedulable — Run Nightly at 01:00
// NightlyMaintenanceScheduler.cls
global with sharing class NightlyMaintenanceScheduler implements Schedulable {
global void execute(SchedulableContext sc) {
// Orchestrate: start the batch
Database.executeBatch(new StaleCaseBatch(/*daysOld=*/30), 200);
}
// One-time setup in Anonymous Apex:
// String cron = '0 0 1 * * ?'; // 01:00 every day
// System.schedule('Nightly Maintenance', cron, new NightlyMaintenanceScheduler());
}
Explanation:
This job serves as the entry point for the nightly process. It runs automatically at 1:00 AM and starts the Batch Apex process that handles stale Cases.
⚙️ B) Batchable — Process Large Datasets in Manageable Chunks
// StaleCaseBatch.cls
global with sharing class StaleCaseBatch implements Database.Batchable<SObject>, Database.Stateful {
private Integer daysOld;
public Integer escalatedCount = 0;
public StaleCaseBatch(Integer daysOld) { this.daysOld = daysOld; }
global Database.QueryLocator start(Database.BatchableContext bc) {
Date cutoff = Date.today().addDays(-daysOld);
return Database.getQueryLocator([
SELECT Id, Status, Priority, OwnerId
FROM Case
WHERE Status != 'Closed' AND LastModifiedDate < :cutoff
]);
}
global void execute(Database.BatchableContext bc, List<Case> scope) {
// 1) Update stale cases in-bulk
for (Case c : scope) {
if (c.Priority == 'High') { c.Status = 'Escalated'; escalatedCount++; }
else { c.Status = 'Awaiting Customer'; }
}
update scope;
// 2) For escalated ones, enqueue a Queueable to notify external
List<Id> escalatedIds = new List<Id>();
for (Case c : scope) if (c.Status == 'Escalated') escalatedIds.add(c.Id);
if (!escalatedIds.isEmpty()) {
System.enqueueJob(new IncidentNotifyJob(escalatedIds));
}
// 3) Tiny telemetry (future)
touchTelemetry(scope);
}
global void finish(Database.BatchableContext bc) {
System.debug('Escalated in total: ' + escalatedCount);
// Optionally chain another batch/queueable here.
}
@future
private static void touchTelemetry(List<Case> casesProcessed) {
// keep tiny: e.g., update a custom object count or log to a Text field
// (no SOQL/DML here for brevity)
System.debug('Telemetry: processed ' + casesProcessed.size() + ' cases');
}
}
Explanation:
The Batch Apex class processes Cases in manageable chunks. It bulk-updates records, chains a Queueable job for escalations, and logs lightweight telemetry using @future.
This approach keeps your logic modular and governor-limit friendly.
? C) Queueable — Handle HTTP Callouts and Controlled Chaining
// IncidentNotifyJob.cls
public with sharing class IncidentNotifyJob implements Queueable, Database.AllowsCallouts {
private List<Id> caseIds;
public IncidentNotifyJob(List<Id> caseIds) { this.caseIds = caseIds; }
public void execute(QueueableContext qc) {
// Rehydrate minimal fields for payload
List<Case> batch = [
SELECT Id, CaseNumber, Subject, Priority
FROM Case WHERE Id IN :caseIds
];
// Callout to external incident system
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:IncidentBridge/escalate');
req.setMethod('POST');
req.setHeader('Content-Type','application/json');
req.setBody(JSON.serialize(batch));
new Http().send(req);
// Optional: chain exactly one more Queueable if you need to paginate
// System.enqueueJob(new IncidentNotifyJob(nextIds));
}
}
Explanation:
Queueable Apex is perfect when you need callouts and flexible job chaining.
This job handles escalated Cases, sends them to an external API, and could chain another Queueable if there’s more data to process.
⚡ D) @future — Simple, Lightweight Fire-and-Forget
// Example: tiny side-effect that doesn't need chaining or complex types
public with sharing class Telemetry {
@future
public static void ping(String category, String message) {
System.debug('Telemetry [' + category + ']: ' + message);
}
}
Explanation:
When you just need a quick, asynchronous action without passing complex data, @future is the simplest and fastest choice.
Use it for non-critical side effects like lightweight logging or marking small events.

? Final Thoughts
When choosing between Salesforce’s async tools, always pick the smallest one that meets your needs:
-
Use @future for simple, “fire-and-forget” tasks.
-
Use Queueable for callouts, chaining, or complex parameters.
-
Use Batch Apex for large data volumes that need to be processed in chunks.
-
Use Schedulable to automate jobs on a time-based schedule.
And remember:
✅ Keep all async jobs idempotent and bulk-safe.
✅ Always log AsyncApexJob IDs for tracking and monitoring.
✅ When in doubt: if it’s large, batch it; if it calls out, queue it; if it’s tiny, future it; if it’s time-based, schedule it.
That’s the mindset of a Salesforce developer who designs async processes that scale cleanly and never surprise in production.
