Using Apex Queueable Jobs to Safely Update Related Records After a Trigger
Learn how to move heavy trigger work into an Apex Queueable job to stay within synchronous governor limits while reliably updating related records.
20 Nov 2025, 14:11 UTC

When a trigger tries to do too much work—such as querying many related records and updating fields—you often hit governor limits like the 100‑SOQL query cap or the 10 000‑row DML limit. The result is a failed transaction and a poor user experience. Moving that work to an asynchronous Queueable job lets you stay within synchronous limits while still guaranteeing the update runs reliably.
Problem: Trigger‑safe work that exceeds synchronous limits
Imagine an Account after‑insert trigger that must find all Contacts belonging to the new Account and set a custom field Territory_Assigned__c based on the Account’s region. If the Account has thousands of Contacts, the trigger will exceed the SOQL or DML limits and roll back.
Solution: Queueable Apex for asynchronous, chained processing
Queueable Apex implements the Queueable interface, allowing you to pass complex objects, chain jobs, and avoid the 50‑job limit of @future. Each Queueable execution consumes one AsyncApex transaction, which has higher limits (e.g., 250 000 executions per day) and runs in its own governor context.
Worked Example: Updating Contacts from an Account Trigger
First, create the Queueable class. It receives the Account ID, queries related Contacts, and updates the custom field.
public class ContactUpdater implements Queueable {
private Id accountId;
public ContactUpdater(Id accountId) {
this.accountId = accountId;
}
public void execute(QueueableContext qc) {
try {
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact c : [SELECT Id, Territory_Assigned__c
FROM Contact
WHERE AccountId = :accountId]) {
c.Territory_Assigned__c = 'West'; // example logic
contactsToUpdate.add(c);
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
} catch (Exception e) {
// Optionally log or rely on Salesforce’s error email
System.debug('ContactUpdater failed: ' + e.getMessage());
}
}
}
Next, the trigger enqueues the job after the Account is inserted.
trigger AccountAfterInsert on Account (after insert) {
for (Account acc : Trigger.New) {
System.enqueueJob(new ContactUpdater(acc.Id));
}
}
Where to run the code: Deploy both the class and trigger to a Developer sandbox using VS Code with SFDX (sfdx force:source:push) or the Developer Console. You need the "Author Apex" permission; no special data‑access rights are required for the query and update shown.
Trade‑off and Limits
Queueable jobs consume AsyncApex limits (default 250 000 executions per 24 hours) and introduce latency—usually a few seconds before the job starts. Synchronous code would be faster but would fail once the trigger’s SOQL/DML limits are exceeded. Monitor consumption with Limits.getAsyncExecutions() and Limits.getLimitAsyncExecutions() in anonymous Apex or Apex Jobs.
Checking the Result
- Insert an Account record via the UI or Data Loader.Wait a few seconds, then open the related Contact list and verify that
Territory_Assigned__cis set as expected.Navigate to Setup → Monitoring → Apex Jobs, locate the Queueable job, and confirm its Status is "Completed".Run anonymous Apex:System.debug('Async used: ' + Limits.getAsyncExecutions() + ' / ' + Limits.getLimitAsyncExecutions());to see the count increase by one.
If the job fails, the Apex Jobs page shows the error and Salesforce sends an email to the job owner (the user who enqueued it). Always include a try/catch block inside execute or rely on that notification to avoid silent failures.
Actionable Closing
Try this pattern in a sandbox for any trigger that needs to touch many related records. Start with a simple Queueable, watch the AsyncApex usage, and adjust the batch size or add chaining if you need to process even larger volumes. This keeps your triggers lean, your users happy, and your org within limits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.