Asynchronous Apex is code that doesn’t run inside the user’s current request. Instead, Salesforce queues it and executes it in a separate thread — often a few seconds later, sometimes minutes later, depending on platform load. The user gets their UI back immediately; the heavy work happens behind the scenes.
The four flavours, in rough order of when you’d reach for each:
| Flavour | Use when | Limits compared to sync |
|---|---|---|
@future | Fire-and-forget tasks: a single HTTP callout, a quick update to records the user can’t see right now | 50 invocations per transaction, no chaining, no return value |
| Queueable | Anything you’d do with @future plus you need to chain jobs, pass complex objects, or track status | 50 per transaction synchronously, only 1 chained at a time |
| Batch Apex | Operating on a lot of records — more than fits in one transaction | Up to 50 million records per job, 200 records per execute by default |
| Schedulable | Running something on a clock (nightly cleanup, weekly digest) | One-shot scheduled, often chains to Batch or Queueable |
Why it exists
Salesforce is multi-tenant. To keep one tenant’s slow code from blocking everyone else, synchronous Apex transactions get hard ceilings: 10 seconds of CPU time, 100 SOQL queries, 150 DML statements, 6 MB of heap, and so on. Real-world workflows — re-rating 2 million Contacts overnight, syncing yesterday’s Opportunities into NetSuite — would never fit inside those ceilings.
Asynchronous contexts get higher limits. Batch Apex, for example, gets 200 SOQL queries per execute and is allowed to keep CPU-time-busy across thousands of executes.
A concrete example: callout from a trigger
You can’t call an external API directly from a trigger — the synchronous request would block the user’s save, and Salesforce explicitly disallows it. The standard pattern:
public class StripeSyncService {
@future(callout=true)
public static void pushAccountToStripe(Set<Id> accountIds) {
List<Account> accounts = [
SELECT Id, Name, Stripe_Customer_Id__c
FROM Account WHERE Id IN :accountIds
];
for (Account a : accounts) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Stripe/v1/customers');
req.setMethod('POST');
// ... set body, send, parse response, write Stripe_Customer_Id__c
}
}
}
trigger AccountTrigger on Account (after insert) {
Set<Id> ids = new Map<Id, Account>(Trigger.new).keySet();
StripeSyncService.pushAccountToStripe(ids);
}
The @future(callout=true) annotation does two things: it queues this method for async execution, and it tells the platform “this method makes an HTTP callout, so don’t run it inside any active transaction.”
When to pick Queueable over @future
Almost always, in 2026. @future is the older mechanism — limited to primitive arguments, no monitoring, no chaining, no completion handler. Queueable was introduced to fix all of those:
public class AccountSyncJob implements Queueable, Database.AllowsCallouts {
private List<Account> accounts;
public AccountSyncJob(List<Account> accs) { this.accounts = accs; }
public void execute(QueueableContext ctx) {
// do the callout, then queue a second job if needed:
if (moreWork()) System.enqueueJob(new AccountSyncJob(remaining));
}
}
If you’re starting fresh on a project, write Queueable. Reserve @future for tiny tasks where you genuinely don’t need any of Queueable’s features.
Common interview follow-ups
- “What’s the difference between
@futureand Queueable?” — Queueable accepts complex types, can be chained, returns a JobId you can monitor, supports callouts viaDatabase.AllowsCallouts. - “When would you choose Batch over Queueable?” — When the data set is too large for one transaction. Batch’s
start → execute (chunks) → finishpattern is built for million-row workloads. - “Can you call a
@futuremethod from another@futuremethod?” — No. The platform blocks@future-from-@futureand@future-from-Batch chains specifically. - “What’s the order of execution if a trigger calls a
@future?” — Trigger finishes, transaction commits,@futureis queued. The async work sees a database state that already reflects the trigger’s writes.
Verified against: Apex Developer Guide — Asynchronous Apex. Last reviewed 2026-05-17 for Spring ‘26 release.