Jan Śledziewski
written by Jan Śledziewski
posted on January 26, 2024
Full-stack Salesforce Developer and Technical Product Owner. On Salesforce trail since 2018. Focused on code quality and solution architecture.
Table of Content

Future vs Queueable


Oftentimes, we need to do an external callout when a trigger transaction is completed, or make an update that requires creating a new transaction, do some summaries at the end of the process.

To do all this stuff, usually, developers would use the @Future method in the first place. But is it the best solution? In this post, I will briefly present future methods and Queueable implementations, then compare them.

Have a good read!

About @Future annotation

The easiest way of achieving asynchronous apex execution is using @Future annotation. By simply adding it to a method, we can run code in an async context.


It's pretty straightforward:

public static void asyncMethod(Id recordId, List<String> primitiveDatatype) {
    // do something

In case of making a callout we would need to use @Future(callout=true).

Then we can execute it:

MyClass.asyncMethod(account.Id, accountAddresses);


The rules are simple:

  • Method has to have the @Future annotation,
  • If a callout is being made, the @Future(callout=true) annotation is needed (by default, it is callout=false)
  • Only data structures (like lists and maps) of primitive types and primitive data types can be passed as parameters
  • Cannot call other future methods, each method is executed in its own asynchronous context

Future methods execute in an asynchronous context, so that the time of execution is not guaranteed. Asynchronous methods run when system resources are available.

Queueable Apex in-depth

Queueable is yet another tool that Salesforce offers for asynchronous processing. It is very similar to @Future methods, but it offers a lot more functionality. Some aspects of the Queueable interface are unique to it own. Simply put, Queueable is more advanced than @Future in most use case scenarios!

The Basics

As you will see, we can pass complex data to Queueable constructor, but we need to implement an entire class.


public class AsynchronousExample implements Queueable {
    private List<SObject> data;
    private Id parentId;

    public AsynchronousExample(List<SObject> records, Id id) {
        this.data = records;
        this.parentId = id;

    public void execute(QueueableContext context) {
        // logic

Starting Queueable job

The simplest way is to use System.enqueueJob:

System.enqueueJob(new AsynchronousExample(records, parentId));

We can also delay the start of Queueable job, for example when integration requires it:

Integer delay = 5; // 0-10 minutes after queueable will be run
System.enqueueJob(new AsynchronousExample(records, parentId), delay);

Or make use of the AsyncOptions class to make additional validations for Stack Depth:

public class AsynchronousExample implements Queueable {
    private static final Integer MAX_DEPTH = 4;

    private List<SObject> data;
    private Id parentId;

    public AsynchronousExample(List<SObject> records, Id id) {
        this.data = records;
        this.parentId = id;

    public static void runJob() {
        AsyncOptions asyncOptions = new AsyncOptions();
        asyncOptions.MaximumQueueableStackDepth = MAX_DEPTH;

        System.enqueueJob(new AsynchronousExample(records, parentId), asyncOptions);

    public void execute(QueueableContext context) {
        List<Sobject> results = doSomeLogic(data, parentId);

        if (System.AsyncInfo.hasMaxStackDepth() && AsyncInfo.getCurrentQueueableStackDepth() >= AsyncInfo.getMaximumQueueableStackDepth()) {
            insert results;
        } else {
            System.enqueueJob(new AsynchronousExample(data, parentId));

    private List<SObject> doSomeLogic(List<SObject> records, Id id) {
        // some secret logic

Okay, we know how to implement it, but does it have any more tricks in its sleeve? The first and obvious one is that we can use complex data types as parameters. Anything else?

Return Job Id

When we enqueue a new Queueable job, we get its Id in return. Why is this important? Remember, Salesforce gives you a promise that an asynchronous job will be eventually run when the resource will be free. But when is that? That's the catch. Using JobId we can verify what happens with our enqueued job.

How to do it?

Id jobId = System.enqueue(AsynchronousExample());
AsyncApexJob jobInfo = [SELECT MethodName, Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];

Learn more about AsyncApexJob here.

Chaining Queueable job

The first advantage of the Queueable interface is that it can chain jobs. Do you remember that Future can't call other Future methods from its own? When running Queueable we can add one more Queueable to the execution queue. Is it a big deal? Yes, it is. It allows us to optimize our execution and split data into simpler blocks, which helps with preventing hitting Governor Limits.

Example use cases:

  • Imagine situation when you have to update contacts based on the account data. It makes sense to split data where all contacts of a given account are in the same chunk.

  • Or we are doing multiple callouts that ones data is required for the other ones, when we split them into separate Queueable transactions, we are getting new Limits for each of the callouts.

How to queue next job?

public void execute(QueueableContext context) {
    // do some logic

    // in execute method, queue next job (but only 1!)
    System.enqueue(new AsynchronousExample(records, id));

Detecting Duplicated Queueable Job

Queueable comes with a great tool AsyncOptions class. Recently, it received another great feature. Now you can add a signature to your Queueable execution, to make sure you are neither wasting resources nor running jobs for the same records twice.

How does it work? Imagine you have Queueable, that uses an account as start point:

public static void runJob(Id accountId) {
    Account account = [SELECT Id FROM Account WHERE Id = :accountId];

    AsyncOptions options = new AsyncOptions();
    options.DuplicateSignature = QueueableDuplicateSignature.Builder()

    try {
        System.enqueueJob(new AsynchronousExample(accountId), options);
    } catch (DuplicateMessageException ex) {
        // Now you know that job for this accountId is already enqueued

From the code above, when QueueableDuplicateSignature is added, we can catch DuplicateMessageException. When the exception is thrown, we are sure that the job for this record is already enqueued.

Transaction Finalizer

What's that? It's neither Future method nor Queueable! Yes, it's not. But System.Finalizer interface is a great addition for your Queueable classes. It gives a Queueable superpowers!


It is similar to Queueable implementation, we need to implement execute method:

public class FinalizerExample implements Finalizer {
    public void execute(FinalizerContext context) {
        System.debug('It\'s not even my final form!');

How will Finalizer know when to be executed? We need to attach it to Queueable job (only one can be attached):

public class AsynchronousExample implements Queueable {
    public void execute(QueueableContext context) {
        System.attachFinalizer(new FinalizerExample());
        // rest of logic

Now each time our Queueable finishes its execution, Finalizer will be run (even when it fails).


As you probably already found out, in Finalizer implementation we are using new context. It comes with few additional methods:

Method Functionality
global Id getAsyncApexJobId ID of the Queueable job which Finalizer is attached to, used to identify job in AsyncApexJob table.
global String getRequestId Unique Id that identifies current request, Queueable job and Finalizer both have the same request Id.
global System.ParentJobResult getResult Returns ParentJobResult enum, possible values are SUCCESS, UNHANDLED_EXCEPTION
global System.Exception getException If an exception occurred, it will return the type of exception.

Handle Limits exception

Have your head about the terrifying System.Limits exception? You don't have to worry about it any more! System.LimitException, can't be usually handled, but what if we use Finalizer, let check out!

I'm creating a dummy Finalizer, just to check if we can run the job oin case of failure:

public with sharing class FinalizerExample implements Finalizer {
    public void execute(FinalizerContext context) {
        System.debug('Job status: ' + context.getResult());

And even worse Queueable implementation:

public class QueueableExample implements Queueable {
    public void execute(QueueableContext context) {
        System.attachFinalizer(new FinalizerExample());

        for (Integer i = 0; i < 201; i++) {
            new List<Account>([SELECT Id FROM Account]);

And without surprise, we got exception:
Too many SOQL Exception 201

But what's that? Did we get Success next?
Success debug log

Yes, we have! It's a message from the Finalizer. Now we can control what happens next, do we send an email with a bug? Requeue job with fewer records? Or just finish there? Thanks to transaction Finalizer we have new options.

For another example, check documentation, you will find there logging example using Finalizer.

If you face any issues with attaching Finalizer to your Queueable job, refer to this Developer Guide page.

Testing Asynchronous Methods

Lastly, of course, we need to check if the code we wrote is working as expected. In both cases, testing is fairly simple:

private class MyTests {

    private static void testFuture() {
        // prepare some data like Account record

        AsynchronousExample.asyncMethod(account.Id, new List<String>{ 'Some', 'Test', 'Strings' });

        // verify @Future method results

    private static void testQueueable() {
        // prepare some data like child records and parent id

        System.enqueue(AsynchronousExample(contacts, account.Id));

        // verify Queueable class results

When testing asynchronous apex, you should always use Test.startTest and Test.stopTest methods. Invoking Test.stopTest makes sure all long-running operations are finished, and you are able to retrieve results from the database.

Summary & Conclusion

As you can see, Future is a fairly simple and easy way to run some code in the asynchronous context. But on the other hand, Queueable is a much more sophisticated solution that offers much more of a functionality. Not only it extends the base functions of @Future annotation, but offers unique aspects. We are able to enqueue another job from inside, check current status or rerun a failed job. All of it is lacking in the @Future. In my mind, in the most cases, we should opt for using Queueable. Its functionalities enable developers to build more robust code which is easily extendable and adoptable.

What makes Queueable better

Most of the Queueable functionalities came from the System.AsyncOptions class and transaction Finalizers. You know about Finalizers already, but remember that System.AsyncOptions provides three powerful functionalities:

Property Functionality
MinimumQueueableDelayInMinutes Allows setting minimum delay time of Queueable job. It serves a purpose during callouts, when multiple jobs are enqueued at the same time, and we want to make sure that we are not overloading the external system. Salesforce Administrators can also specify org-wide delay, more on that in the documentation.
MaximumQueueableStackDepth Is more of an optimization tool for developers. Prevents too many nested Queueable execution in one transaction. Developers can set the desired stack before execution, preventing Limit exception.
DuplicateSignature Is also an optimization tool. The developer can implement a signature for a Queueable job, which is used during Queueable execution to prevent running code for the same data. It's especially useful when we have Queueable with multiple entry points (places where we enqueue it), with a good signature we can ensure that the job will be run only once for selected records

When to use what

As I wrote, we should use Queueable in the most cases, but when to use @Future then?

Use Case Method
Callout for external data or simple operations Future
Trigger update on the same object and other workarounds (Warning! Potential exception can occur when recursion happens!) Future
Also for callouts, when multiple long-running timeouts are needed, but also useful for a robust integration frameworks thanks to easy processing control, deduplication capabilities and easier logging. Queueable
When big amounts of data needs to be processed. @Future method gives us only one asynchronous transaction. Meanwhile, in Queueable we can split data into chunks and process them in multiple chained jobs. Queueable
Critical operations, thanks to Finalizer we can reschedule a job, even if we got Limits exception. Queueable
When we require asynchronous methods with better observability and optimization. Simple returning JobId gives a lot of insight on what is going on with the job. Queueable