Queueable Apex: The Middle Ground Between @future and Batch Jobs
Queueable Apex is the underused middle ground between @future and Batch Apex: complex arguments, fresh governor limits, and job chaining — with one sharp fan-out limitation to design around.
11 Apr 2026, 09:41 UTC

Your trigger recalculates a handful of related records, updates them, and fires off a notification. It works in testing, then falls over in production with a governor limit error because one account has 4,000 related contacts. The obvious fixes — move the work out of the synchronous request — come with a fork in the road: @future, Batch Apex, or Queueable. For a lot of real workloads, Queueable is the right answer, and it's underused.
The thesis: Queueable Apex should be your default for one-off or sequentially chained asynchronous work. Reach for Batch Apex when you're processing record sets in the hundreds of thousands, and treat @future as legacy.
What Queueable actually gives you
A Queueable class implements System.Queueable and defines an execute(QueueableContext context) method. You start it with System.enqueueJob(new MyJob(...)), which returns a job ID you can look up in the AsyncApexJob table or under Setup > Apex Jobs. It's been generally available since Winter '15, so this is mature, well-established platform behavior.
Three properties matter in practice:
- Complex arguments. Unlike
@futuremethods — which only accept primitives and collections of primitives — a Queueable job is a real object. You can pass sObjects, custom Apex types, and collections into the constructor. That alone eliminates the "query everything again by ID" boilerplate that@futureforces on you. - A fresh set of governor limits. The job runs in its own transaction with its own SOQL and DML limits, so work that would blow up a synchronous request gets breathing room.
- Chaining. From inside
execute(), you can enqueue exactly one child job. That enables sequential pipelines: step one recalculates, step two notifies, step three syncs to an external system.
A worked example: recalculate, then notify
Say an account update requires recalculating aggregated data across its contacts, and only after that finishes should notifications go out. Doing both synchronously risks hitting limits; doing them as two independent async jobs risks the notification running before the recalc. Chaining solves the ordering:
public class AccountRecalcJob implements Queueable {
private Set<Id> accountIds;
public AccountRecalcJob(Set<Id> accountIds) {
this.accountIds = accountIds;
}
public void execute(QueueableContext context) {
List<Account> accounts = [
SELECT Id, (SELECT Id, Status__c FROM Contacts)
FROM Account WHERE Id IN :accountIds
];
// ... recalculation logic, then update accounts ...
// Chain exactly one follow-up job
System.enqueueJob(new AccountNotifyJob(accountIds));
}
}Enqueue the first job from a trigger handler or from Anonymous Apex in a sandbox:
Id jobId = System.enqueueJob(new AccountRecalcJob(accountIds));
System.debug('Enqueued: ' + jobId);To verify it ran, query SELECT Id, Status, JobType, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId in the Developer Console, or check Setup > Apex Jobs. You should see both the parent and the chained child job once the parent completes. No special permissions are needed to enqueue a job, but you do need Author Apex (or a sandbox) to deploy the class.
The one-child-job rule, and how to design around it
The sharpest edge: a single Queueable execution can enqueue only one child job. Call System.enqueueJob twice inside execute() and you get a LimitException. This kills naive fan-out designs where one job spawns ten parallel workers.
The practical workarounds:
- Enqueue the fan-out from the caller. If you know upfront you need ten parallel jobs, enqueue all ten from the synchronous context (or from a Batch
start()method) rather than from inside a job. - Process a list serially via chaining. Pass the remaining work items in the constructor; each job handles one and chains the next. This gives you ordering and limit isolation at the cost of latency.
- Use Batch Apex when the fan-out is really "process these 2 million records." Batch chunks records into scoped executions automatically — that is precisely the problem it exists to solve.
Note that chaining depth limits vary by org edition and have changed across releases — developer and trial orgs have historically been capped at a shallow depth (commonly 5), while production orgs allow much deeper chains. Verify the current numbers in the Apex Developer Guide for your release before designing a deep pipeline.
Trade-offs you accept
Asynchronous means no immediate answer. The user who clicked save doesn't know whether the recalc succeeded; you need monitoring (a scheduled check on AsyncApexJob failures, or Platform Events for status) if the result matters operationally. Jobs are also not guaranteed to start immediately — under org load they queue, so Queueable is wrong for latency-critical paths. And independent jobs have no ordering guarantee; only chaining gives you sequence. Debugging is harder too: debug logs for async jobs appear as separate entries, and failed jobs don't retry automatically.
When to pick what
| Need | Choose |
|---|---|
| Simple fire-and-forget, primitives only | Queueable anyway — @future has no advantage left for new code |
| Heavy one-off work with complex inputs | Queueable |
| Strictly ordered multi-step pipeline | Chained Queueable |
| Hundreds of thousands+ of records | Batch Apex |
| Result needed by the user right now | None of these — do it synchronously or redesign the UX |
The actionable version: next time you write @future, stop and write a Queueable class instead — you'll get typed inputs, a trackable job ID, and a chaining option when the workflow grows a second step. Prototype it in a free Developer Edition org: enqueue from Anonymous Apex, confirm both parent and child in AsyncApexJob, and try enqueueing a second child to see the LimitException for yourself. That fifteen-minute exercise will tell you more than any limit table.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.