ApexSensei

Blog

Queueable Apex: When and How to Use It

By Firus Hanov · · 8 min read

Queueable Apex vs @future, Batch, and Schedulable: a short example, the limits that matter, common mistakes, and matching ApexSensei drills.

Queueable Apex: When and How to Use It

Queueable Apex is the usual way to continue work after the current Salesforce transaction finishes. You implement Queueable, call System.enqueueJob, and Salesforce runs execute later in its own transaction.

Salesforce now recommends Queueable over @future for new work. You get a job Id, you can pass sObjects and custom types, and you can chain one follow-up job. The official guide is Queueable Apex in the Apex Developer Guide.

When Queueable is the right tool

Use Queueable when the current request must finish, and a bit more work should run soon after: a callout, a second DML sequence, or a follow-up that needs the records you just saved.

Use @future only when you already have a static method and you only need primitives or Ids. Future methods cannot be chained in a way you can monitor, and they do not return a job Id to your code.

Use Batch Apex when the set is large enough to need a QueryLocator and many scopes. A Queueable that queries 50,000 rows into a List is still one transaction. A batch execute is a new transaction per scope.

Use Schedulable when the work must start at a clock time. The scheduled class can enqueue a Queueable or start a batch. Remember that scheduled Apex uses synchronous governor limits, not the async column.

A short, correct example

This job updates one Account after some other work has already committed. The class holds the Id as a member. enqueueJob returns the AsyncApexJob Id so you can query status later.

public class AccountReviewJob implements Queueable {
    private final Id accountId;

    public AccountReviewJob(Id accountId) {
        this.accountId = accountId;
    }

    public void execute(QueueableContext context) {
        Account row = [
            SELECT Id, Description
            FROM Account
            WHERE Id = :accountId
        ];
        row.Description = 'Reviewed ' + Datetime.now();
        update row;
    }
}

Id jobId = System.enqueueJob(new AccountReviewJob(accountId));

If the caller transaction rolls back, Salesforce does not run the queued job. That is documented behavior, not a bug.

Need a callout? Implement Database.AllowsCallouts on the same class. Do the DML in the original transaction, then let the Queueable talk to the remote system.

The matching drill is Carry account work in a Queueable.

Limits that actually bite

These figures are from Salesforce's Queueable guide and the Execution Governors and Limits table. They are not house rules.

  • A synchronous transaction can add 50 jobs with System.enqueueJob.
  • An asynchronous transaction (a batch execute, another Queueable, a future method) can add 1 job.
  • From a running Queueable you may enqueue only one child. One parent, one child. No fan-out.
  • Developer Edition and Trial orgs cap the chain at 5 jobs, including the first one. You can chain four times. Other editions do not enforce a depth cap, but each hop still counts against the shared daily async limit.
  • Each queued job counts once against the org's shared asynchronous Apex executions. Salesforce documents that daily cap as 250,000, or user licenses times 200, whichever is greater.
  • The job itself runs with async transaction limits: 200 SOQL queries, 12 MB heap, 60,000 ms CPU. DML stays at 150 statements and 10,000 rows.

Limits.getQueueableJobs() tells you how many you have already enqueued in the current transaction. Check it before you enqueue in a loop.

You can pass a delay of 0–10 minutes to System.enqueueJob(job, delay). Use that when you are polling an external system. A tight chain with no delay is how teams burn the daily async cap before lunch.

Common mistakes

Enqueuing from a loop for every record. Fifty Accounts can be one job that takes a List. Fifty jobs from one trigger batch waste the 50-job cap and the daily async pool.

Starting two children from one execute. Salesforce rejects that. If you have two follow-up steps, run the second as the child of the first. The drill is Chain a second Queueable.

Doing work that is not safe to retry. Jobs fail. A finalizer or an idempotent write (check a status field before you insert again) is the difference between one follow-up Task and twelve. See Make a Queueable retry idempotent and Attach a Queueable finalizer.

Using Queueable for a 2 million row sweep. That is Batch Apex. Queueable is a second transaction, not a cursor over the org.

Calling a future method when you needed a job Id. You cannot query AsyncApexJob for a future the way you can for a Queueable. If an admin has to see the job on the Apex Jobs page, use Queueable.

How this compares, in one line each

  • @future — fire and forget, primitives only, no job Id in your code.
  • Queueable — job Id, sObjects, one child, callouts with AllowsCallouts.
  • Batch — QueryLocator up to 50 million rows, many scopes, five active jobs, flex queue of 100.
  • Schedulable — clock time. Then enqueue or executeBatch from execute(SchedulableContext).

Complete a Contact review in the future is the future-method drill. Schedule account review is the Schedulable one. The governor table that all four share is in Salesforce Governor Limits: Sync vs Async.

Practice Apex free on ApexSensei