Log Entry

Queueable Apex That Survives Retries

Jun 16, 2026 · 14 min read

Queueable Apex makes asynchronous work easy to start.

Reliability begins after System.enqueueJob() returns.

The job can fail because of a callout, a lock, malformed data, a limit, or an unhandled code path. A user can click twice. Automation can enqueue the same logical work twice. An operator can retry a job whose first attempt completed the remote call but failed before recording success.

The dangerous assumption is:

One enqueue means one execution of the business effect.

I design important jobs as if the work can be attempted more than once.

Quick snapshot

A retry-safe Queueable design needs:

  • a business idempotency key
  • a durable record of state or completion
  • a clear distinction between transient and permanent failure
  • a maximum number of attempts
  • delay or backoff between attempts
  • small, resumable units of work
  • useful monitoring data

The job ID is useful operational data. It is not normally the business idempotency key.

Start with the business operation

Suppose a Queueable sends an order to a fulfilment API.

The unsafe design is:

public void execute(QueueableContext context) {
    Order__c orderRecord = loadOrder(orderId);
    fulfilmentClient.submit(orderRecord);
    orderRecord.Status__c = 'Submitted';
    update orderRecord;
}

What happens if the remote service accepts the order, then the Salesforce update fails?

A retry can submit the order again.

The first question is not "how do I catch the exception?"

It is "how will the remote operation recognise this same business request?"

Use an idempotency key

Create a stable key for the logical operation, for example:

fulfil-order:801xx0000001234:v1

Send it to a remote API that supports idempotency. If the remote system does not support that directly, agree another deduplication contract, such as an external order reference with a uniqueness constraint.

The key must survive retries. A new random value on every attempt defeats the point.

Within Salesforce, a unique field on a work record can prevent duplicate logical work from being created in the first place.

Keep durable work state

For significant jobs, I like a small work record containing fields such as:

  • operation key
  • target record ID
  • current status
  • attempt count
  • next attempt time
  • last error code
  • last error summary
  • external reference
  • completion time

That record is not bureaucracy. It answers the operational questions that AsyncApexJob alone cannot:

  • Which business operation was this?
  • Did a later attempt succeed?
  • Was the remote system updated?
  • Is somebody expected to intervene?

Use statuses with defined meaning, such as Pending, Running, Retry Scheduled, Completed, and Failed Permanently.

Avoid a vague Error status that says nothing about what happens next.

Claim work before performing it

Two jobs can race for the same logical operation.

The job should load and claim its work record using a locking or atomic state-transition strategy appropriate to the design. It should exit safely if the work is already complete or another valid worker owns it.

Async_Work__c workItem = [
    SELECT Id, Status__c, Attempt_Count__c, Operation_Key__c
    FROM Async_Work__c
    WHERE Id = :workItemId
    FOR UPDATE
];

if (workItem.Status__c == 'Completed') {
    return;
}

FOR UPDATE is not a universal distributed lock. It protects the Salesforce transaction. The external idempotency contract still protects the external effect.

Classify failures

Not every failure deserves a retry.

Transient examples:

  • HTTP 429 rate limiting
  • temporary 5xx response
  • record lock contention
  • a network timeout where the remote command is idempotent

Permanent examples:

  • invalid configuration
  • missing required business data
  • authentication that requires intervention
  • an unsupported request rejected by the remote system

Blindly retrying permanent failures consumes async capacity and delays the moment somebody fixes the actual problem.

Return or throw failure types that preserve this distinction.

Bound the retry policy

Every retry loop needs an end.

For example:

4 / 4 rows

1Immediate
25 minutes
320 minutes
460 minutes

The exact policy depends on the business deadline and dependency.

The principles are stable:

  • cap attempts
  • increase delay for shared-service pressure
  • record why the next attempt is allowed
  • move exhausted work to a visible terminal state

Queueable Apex supports delayed execution options, but the retry policy still belongs to the application. Platform capability does not decide whether retrying is correct.

Make the unit of work resumable

A job that processes 50,000 unrelated items and records success only at the end is difficult to retry safely.

Prefer bounded units with explicit checkpoints:

  • one external order command
  • one page from an Apex cursor
  • one partition of a repair operation
  • one group sharing a transaction requirement

This connects directly to the value of Apex cursors: a cursor and position make continuation state explicit instead of forcing a failed job to rediscover its progress.

Use finalisers for observation and controlled continuation

A Queueable transaction finaliser runs after the Queueable finishes and can inspect its outcome. That makes it useful for centralised monitoring, cleanup, and carefully controlled retry decisions, including failures that escaped the job's normal code path.

It does not make a non-idempotent operation safe.

A finaliser can decide that another attempt is allowed. The business design must ensure that another attempt cannot duplicate an already completed effect.

Monitor business outcomes, not only Apex jobs

An AsyncApexJob status of Completed means the Apex job completed.

It does not necessarily mean the customer's order reached the required business state.

Good monitoring joins the technical and business views:

  • Salesforce job ID
  • operation key
  • attempt number
  • target record
  • external reference
  • final business status

That is the difference between "the job ran" and "the work is done".

Test the ugly paths

The important tests are not only the happy-path Queueable test.

Test:

  • duplicate enqueue or duplicate work creation
  • an already-completed work item
  • transient failure followed by success
  • permanent failure
  • exhausted attempts
  • failure after the external response but before local completion
  • two workers attempting to claim the same work

Some concurrency and external-system behaviour needs integration testing beyond an Apex unit test. Keep the idempotency and state-transition logic in focused services so as much as possible remains deterministic and testable.

The mental model

Queueable is a delivery mechanism for work.

It is not a guarantee that the business effect happens once.

Once I treat asynchronous execution as repeatable delivery, the design changes in useful ways: stable keys, explicit states, bounded retries, small checkpoints, and monitoring that speaks the language of the operation.

That is what makes a Queueable job survivable.