[readonly] markdown buffer
Queueable Apex That Survives Retries
Queueable Apex makes work easy to start. Reliability begins after System.enqueueJob() returns.
A callout can time out, a record can lock, a user can click twice, or an operator can retry a job whose remote effect completed before Salesforce recorded success.
The dangerous assumption is that one enqueue produces one business effect.
Important asynchronous work should be designed for more than one attempt.
Protect the business operation
Suppose a Queueable submits an order to a fulfilment API, then updates the Salesforce order.
public void execute(QueueableContext context) {
Order__c orderRecord = loadOrder(orderId);
fulfilmentClient.submit(orderRecord);
orderRecord.Status__c = 'Submitted';
update orderRecord;
}
If the remote call succeeds and the update fails, a retry can submit the order twice.
- 01submit fulfilmentshipment created
- 02update Salesforcerecord lock
- 03retry jobnew attempt
- 04submit fulfilmentsecond shipment
fulfil-order:801…:v1- 01claim operationattempt 1
- 02remote accepts keyshipment created
- 03retry same keyexisting result
- 04record completioncheckpoint saved
The solution starts with a stable business idempotency key, such as fulfil-order:801xx0000001234:v1. Send it to an API that supports idempotency or agree another deduplication contract. A new random value on every attempt defeats the point.
The Salesforce job ID remains useful operational data. It is not the identity of the business operation.
Make progress durable
Significant jobs need a work record containing the operation key, target, status, attempt count, next attempt time, last error, external reference, and completion time.
That record answers what AsyncApexJob cannot: which operation this was, whether a later attempt succeeded, what happened remotely, and whether somebody must intervene.
Before performing work, claim the record and exit safely when it is already complete.
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 protects the Salesforce transaction. The idempotency key still protects the external effect.
- target
- Order 801…1234
- attempt
- 1 / 3
- external ref
- —
- next attempt
- 14:02:30
- 01callout timed outretryable
- 02backoff scheduled30 seconds
- 03business operation completedSHIP-48291
Keep units of work small and resumable: one external command, cursor page, repair partition, or group that genuinely shares a transaction. A job that processes everything and records progress only at the end is difficult to retry safely.
Retry only what can recover
Rate limits, temporary server errors, record locks, and safe network timeouts may justify another attempt. Invalid configuration, missing business data, and unsupported requests usually do not.
Blindly retrying permanent failures consumes capacity and delays intervention.
Every policy needs a cap, increasing delay, and a visible terminal state. Queueable delay features provide mechanics; the application still decides whether retrying is correct.
A transaction finaliser can observe escaped failures and schedule controlled continuation. It cannot make a non-idempotent operation safe.
Monitor the outcome, not the job
An Apex job marked Completed only proves that Apex finished. It does not prove the customer's order reached its required state.
Monitoring should connect the job ID, operation key, attempt, target record, external reference, and final business status.
Test duplicate delivery, already-completed work, transient failure followed by success, permanent failure, exhausted attempts, and failure after a remote response but before local completion.
Queueable is a delivery mechanism, not an exactly-once guarantee.
Once work is treated as repeatable delivery, the design becomes clearer: stable identity, durable state, bounded retries, resumable checkpoints, and monitoring that speaks the language of the business operation.