Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
A diagram illustrating the flow and processing steps of Queueable Apex in Salesforce development
Apex

Mastering Queueable Apex in Salesforce

The short answer

Queueable Apex is the asynchronous framework in Salesforce that accepts complex data types, hands back a Job ID you can monitor, and lets you chain jobs. Use it to push heavy work off the synchronous transaction and away from its governor limits.

Key takeaways Pass complex data structures such as sObject lists into the class constructor instead of being limited to primitive parameters. Add a maximum depth check to your chaining logic so a job cannot loop forever or run past the platform chaining limit. Split setup and non-setup object updates into separate Queueable jobs to avoid mixed DML errors. Use the Transaction Finalizer interface for error recovery and cleanup rather than relying on a try-catch inside the execute method. Enqueue the job between Test.startTest() and Test.stopTest() in test classes so it runs synchronously before your assertions.

If you're hitting limits in your synchronous code, Queueable Apex is usually the first tool I recommend. I've spent years fixing performance issues in messy orgs, and this is the middle ground developers keep forgetting about. It's lighter to write than Batch Apex and far more capable than a simple @future method.

A trigger starts failing on the 101 SOQL limit, or a callout takes too long, and the work has to come off the transaction. Deciding between Asynchronous Apex in Salesforce types can be tricky, but Queueable is where I land most of the time.

Why I prefer Queueable Apex over future methods

I used to use @future for everything back in the day. It has real flaws though. You can't pass complex objects like lists of sObjects or custom classes, so you're stuck with primitives. Queueable Apex fixes that: you pass actual objects into the constructor, which saves a lot of unpacking when the logic gets complicated.

Monitoring is the other reason I reach for it. Fire off a Queueable job and you get a Job ID back immediately, so you can find it in the Apex Jobs queue and see whether it succeeded or failed. With @future you're mostly hoping for the best. You can also chain jobs, which matters when managing Salesforce large data volumes and things have to happen in a specific order.

A simple implementation that actually works

Here's how I usually structure these classes. The example updates some Account descriptions in the background, and the pattern is what matters. Note how the data comes in through the constructor.

public class AccountUpdateQueueable implements Queueable {
    private List<Account> accountsToUpdate;
    private String updateReason;
    
    public AccountUpdateQueueable(List<Account> accounts, String reason) {
        this.accountsToUpdate = accounts;
        this.updateReason = reason;
    }
    
    public void execute(QueueableContext context) {
        try {
            for (Account acc : accountsToUpdate) {
                acc.Description = 'Updated: ' + updateReason + ' on ' + DateTime.now();
            }
            if (!accountsToUpdate.isEmpty()) {
                update accountsToUpdate;
            }
        } catch (Exception e) {
            // Log this to a custom object so you don't lose the error
            System.debug('Error in Queueable: ' + e.getMessage());
        }
    }
}

Kicking it off is a one-liner:

Id jobId = System.enqueueJob(new AccountUpdateQueueable(myAccounts, 'Bulk Update'));

Best practices for Queueable Apex in the wild

I've seen teams get into trouble by chaining jobs infinitely. Salesforce caps this for a reason. A production org lets you chain a long sequence of jobs, while a Developer Edition or trial org allows far fewer. If you're chaining, put a "max depth" check in your code or you'll eventually hit a wall.

Use the Transaction Finalizer interface if you need to handle errors or clean up after a job. It's more reliable than trying to wrap everything in a try-catch block inside the execute method.

A few things I keep in mind when I'm building with Queueable Apex:

  • Use Limits.getDmlStatements() and Limits.getQueries() so you know how close to the edge you are.
  • Bulkify. Don't pass one record if you can pass fifty; it's much cheaper for the platform.
  • If you're updating a User (setup object) and an Account (non-setup object), do it in separate jobs to avoid that annoying "MIXED_DML_OPERATION" error.
  • Use Test.startTest() and Test.stopTest() in your tests. The job won't actually run until you hit that stop call.

The async limit trips people up too. I've written before about how to stay under asynchronous limits, and it's worth a read if you're planning on running thousands of these jobs a day. You don't want to be the one who shuts down the org's automation by blowing through the 24-hour limit.

Key takeaways

  • Queueable Apex handles complex data types like Lists and Sets, unlike @future methods.
  • Every job returns a Job ID, so it's easy to track in the UI.
  • Chaining gives you sequential processing, but it needs guardrails against infinite loops.
  • An enqueued job only runs during a unit test once you reach Test.stopTest().

Queueable Apex is mostly about making the app feel faster for the user. Nobody wants to wait five seconds for a page to save while your code talks to an external API or updates 200 related records. Move that logic to the background, and the code holds up better for it.

Frequently asked questions

Why use Queueable Apex instead of future methods?

Queueable Apex takes complex data types such as sObjects and custom classes through the class constructor, returns a Job ID you can track, and supports sequential job chaining.

How do you test Queueable Apex?

Enqueue the job between Test.startTest() and Test.stopTest() in your test method. The job runs synchronously at Test.stopTest(), so you can query the results and assert on them after that line.

How do you avoid mixed DML errors in Salesforce?

Split work on setup objects such as User and work on non-setup objects such as Account into separate asynchronous jobs.

How do you handle errors in Queueable Apex?

Use the Transaction Finalizer interface to handle errors and clean up after the job. It recovers more reliably than wrapping the logic in try-catch blocks.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment