Choosing Between @future and Queueable Apex for Asynchronous Work
Decide when to use Salesforce @future methods versus Queueable Apex based on parameter types, job chaining, and monitoring needs.
15 Aug 2026, 10:54 UTC

Decision: pick the right asynchronous mechanism
You need to run Apex outside the synchronous transaction, typically to avoid the mixed DML error or to make a callout after DML. Salesforce offers two lightweight asynchronous options: @future methods and Queueable Apex. Both run in a separate transaction with higher asynchronous governor limits, but they are not interchangeable.
Use this decision rule:
- If the task only needs primitive values or collections of primitives and does not need to be tracked or chained, use
@future. - If you must pass sObjects, custom objects, or other non-primitive types, chain dependent jobs, or inspect execution status by job ID, use
Queueable.
Constraints that shape the choice
A @future method is a static method annotated with @future. It accepts only primitive data types—such as Integer, String, ID, Boolean, Date, or Decimal—or collections of those primitives. It cannot accept an sObject, a list of sObjects, or a custom Apex class.
A Queueable class implements the Queueable interface and defines an execute(QueueableContext context) method. Because the constructor can accept any Apex type, you can pass sObject lists, maps, or custom wrapper classes into the job. System.enqueueJob() returns an ID you can use to look up the job in AsyncApexJob.
Option comparison
| Factor | @future method | Queueable Apex |
|---|---|---|
| Allowed parameter types | Primitives and collections of primitives only | Any Apex type, including sObjects and custom classes |
| Job chaining | Not supported; calling @future from @future causes a runtime error | Supported; call System.enqueueJob() from inside execute() |
| Job identifier | No ID returned | Returns an ID for status tracking |
| Monitoring | Apex Jobs page in Setup | Apex Jobs page plus AsyncApexJob queries by ID |
| Typical pattern | Fire-and-forget cleanup or callout after DML | Multi-step orchestration and complex data processing |
Trade-offs
@future is the simplest syntax and requires the least code, but it is inflexible. You cannot pass an account record to it, and you cannot start a second job from inside the first. Queueable requires more boilerplate—an interface implementation and an explicit enqueue call—but it handles non-primitive parameters and sequential job chains. Both mechanisms share the same asynchronous execution limits, so neither protects you from exceeding the organization-wide queue limit.
Implementation: enqueue a Queueable job that processes accounts
This example runs in Anonymous Apex (Developer Console or the VS Code Salesforce extension). You need permission to create or run Apex in the org; in most sandboxes and Developer Editions, standard developer access is sufficient. In production, deploying Apex requires the Modify All Data permission or deployment through a change set or CLI with appropriate access.
public class AccountStatusQueueable implements Queueable {
private final List accounts;
public AccountStatusQueueable(List accounts) {
this.accounts = accounts;
}
public void execute(QueueableContext context) {
for (Account a : accounts) {
a.Status__c = 'Processed';
}
update accounts;
}
}
To enqueue it:
List accounts = [SELECT Id, Status__c FROM Account LIMIT 10];
ID jobId = System.enqueueJob(new AccountStatusQueueable(accounts));
Do not assume the job has completed immediately. Asynchronous execution is queued and may run seconds or minutes later depending on system load.
Validation: confirm the job ran
- Capture the
IDreturned bySystem.enqueueJob(). - Open Setup > Apex Jobs and locate the job by ID or by the class name
AccountStatusQueueable. - Run this SOQL query in Developer Console or the Query Editor, replacing the placeholder with the captured ID:
SELECT Id, Status, JobType, ExtendedStatus, NumberOfErrors
FROM AsyncApexJob
WHERE Id = '707xxxxxxxxxxxx'
Expected results: JobType is Queueable. Status moves from Processing or Queued to Completed. If the job failed, ExtendedStatus contains the exception message.
For comparison, this @future signature will not compile because it accepts a list of sObjects:
// Compile error: Formal parameter type must be primitive or collection of primitive
@future
static void badExample(List accounts) {}
Limitations and operational risks
@futuremethods cannot call other@futuremethods; the runtime throws an error.- Queueable chaining is supported, but older API versions may enforce stricter limits on the number of chained jobs per transaction. Use the latest API version for your classes unless a managed package requires otherwise.
- Both options count toward the asynchronous execution limit. Excessive enqueueing can produce
Too many queueable jobs added to the queueor equivalent async limit errors. - Execution time is not guaranteed. Do not design user-facing flows that depend on a Queueable job finishing before the next page load.
Practical check before you commit
Because System.enqueueJob() changes state by adding a job to the queue, validate the input data and the class logic in a sandbox first. There is no reliable programmatic rollback after a job is enqueued. If a job has not started yet, an administrator can abort it from Setup > Apex Jobs; otherwise, correct the data and let the job complete or fail, then run a compensating update if needed.
Choose @future for simple primitive-only tasks. Choose Queueable when you need to pass records, chain work, or verify completion through a job ID.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.