Reach for asynchronous Apex when the work doesn’t need to complete inside the user’s request and would either annoy the user, exceed sync governor limits, or fail outright in a sync context. The four canonical reasons: callouts from triggers, long-running jobs, large data volumes, and scheduled work.
The four classic triggers for going async
1. Callouts from triggers
Synchronous HTTP from a trigger throws CalloutException — the platform explicitly disallows it. Anything that requires an outbound API call from a trigger must go through @future(callout=true) or a Queueable implementing Database.AllowsCallouts.
trigger AccountTrigger on Account (after insert) {
StripeService.createCustomers(Trigger.newMap.keySet());
}
public class StripeService {
@future(callout=true)
public static void createCustomers(Set<Id> ids) { /* ... */ }
}
2. Work that exceeds sync governor limits
Synchronous Apex gets 10 seconds of CPU, 100 SOQL queries, 150 DML statements, 6 MB heap. Real workloads — re-rating millions of contacts, recomputing roll-ups across years of opportunities — blow past these instantly.
Batch Apex gets a fresh quota on every execute() chunk. You can process 50 million records by paying the limit once per 200-record batch instead of once for the whole dataset.
3. Long user-facing operations
If a button click takes 8 seconds to import a CSV and update related records, the user’s browser is locked up for 8 seconds. Push the work to a Queueable, give the user a “Job submitted — we’ll notify you when it’s done” toast, and return control immediately.
4. Scheduled / recurring jobs
Anything that should run on a clock — nightly sync, weekly digest, monthly archive — needs Schedulable Apex. The sync world has no concept of “run this Apex at 2 AM.”
A more complete decision table
| Scenario | Reach for |
|---|---|
| HTTP callout from a trigger | @future(callout=true) or Queueable + Database.AllowsCallouts |
| Process > 10,000 records | Batch Apex |
| Process > 50 million records | Batch Apex with Database.QueryLocator |
| User-initiated job that takes > 2 seconds | Queueable |
| Chain dependent jobs | Queueable (one job enqueues the next) |
| Run on a schedule | Schedulable, often calling Batch or Queueable |
| Decouple integration from save | Platform Event + Apex trigger on the event |
| Email after save | The after trigger; Messaging.sendEmail itself runs in commit phase |
When NOT to use async
- The user needs to see the result before their next click. Async runs in a separate transaction — by definition the user has already moved on.
- The work fits comfortably in sync limits and finishes in under a second. Async has overhead and isn’t free.
- You need transactional consistency with the trigger’s DML. Async runs after the commit; it sees the post-commit database and cannot roll back the original transaction.
Common interview follow-ups
- Why does Salesforce ban sync callouts from triggers? — Multi-tenancy. A slow callout would hold the database transaction open and starve other users.
- What’s the latency before async runs? — Usually seconds, sometimes minutes under load. No SLA. Design for “eventually” not “immediately.”
- Can I see the result of an async job? — Track JobId in
AsyncApexJob(or query it directly). Queueable returns the JobId synchronously when you enqueue it.
Verified against: Apex Developer Guide — Asynchronous Apex Overview, Execution Governors and Limits. Last reviewed 2026-05-17.