- Fix Guides
- How to Fix Salesforce Record Locking Exceptions (UNABLE_TO_LOCK_ROW)
How to Fix Salesforce Record Locking Exceptions (UNABLE_TO_LOCK_ROW)
Step-by-step fix guide with AI-powered diagnosis from BuildForce.
Salesforce throws UNABLE_TO_LOCK_ROW when two concurrent transactions try to update the same record or share a parent record on a master-detail relationship. Common triggers: parallel Bulk API batches updating child records of the same parent, Apex triggers that lock parent records for inventory or aggregate updates, and integrations that update the same record from multiple sources within the same 10-second window. The fix is to serialize parent-affecting updates (use Bulk API serial mode), explicitly use FOR UPDATE in critical SOQL queries to acquire locks deterministically, and implement retry-with-jitter in Apex for transient lock failures.
Symptoms
Bulk API batches returning UNABLE_TO_LOCK_ROW for 5-20% of records
Apex triggers throwing 'unable to obtain exclusive access to this record' during business-hour spikes
Inventory updates intermittently double-counting or under-counting
Reports showing transient sync gaps that resolve on retry
DML on Opportunity throwing lock error when the parent Account is being updated by another transaction
Same record locked-failure repeating in tight loops with no apparent contention source
Root Causes
Master-detail parent lock during child DML
When you update a child record on a master-detail relationship, Salesforce locks the parent record for the duration of the transaction. Two simultaneous child-updates on the same parent block each other, throwing UNABLE_TO_LOCK_ROW on the loser.
Bulk API parallel mode on shared parents
Bulk API 2.0 defaults to parallel batch processing. If 200 child records share one Account, two batches running in parallel both attempt to lock that Account row, and one fails with UNABLE_TO_LOCK_ROW per child record.
Apex trigger reading without FOR UPDATE then updating
A read-then-update pattern in Apex without FOR UPDATE doesn't acquire the lock at read time. Between the read and the write, another transaction can modify the row, and the write fails with UNABLE_TO_LOCK_ROW.
Cross-trigger recursion locking same record
Trigger A updates Account.X, which fires Trigger B which updates the same Account, which loops back to Trigger A. The recursive transaction holds the lock when Trigger A's second pass tries to re-acquire it.
Integration updates from multiple sources in the same second
When HubSpot, Marketo, and Pardot all push to the same Contact within a few hundred milliseconds, the third update finds the record locked by the first two and fails. Without retry logic, the integration drops the update.
How to Fix It — Step by Step
Identify the lock contention pattern from the error
UNABLE_TO_LOCK_ROW errors include the record ID and the operation. Group failures by record ID — if many failures point to the same parent record (Account, Master), you have parent-lock contention. If they're scattered, it's row-level contention from integrations.
Switch Bulk API jobs to Serial mode for parent-affecting loads
For Bulk API 2.0 loads that update child records sharing parents, create the job with concurrencyMode=Serial (legacy Bulk v1) or order the input CSV so all rows for one parent are contiguous in the same batch (Bulk v2 batches based on order).
Use FOR UPDATE in critical Apex read-then-write paths
When Apex reads a record and then updates it based on the read value (counters, inventory, aggregates), use FOR UPDATE to acquire the lock at read time. The lock holds until the transaction commits.
// Inventory decrement pattern
Account acc = [SELECT Id, Inventory_Count__c FROM Account WHERE Id = :accountId FOR UPDATE];
acc.Inventory_Count__c -= 1;
update acc;Implement retry with exponential backoff and jitter
For integration callers, wrap DML in retry logic. On UNABLE_TO_LOCK_ROW, wait 200ms × 2^attempt + jitter, then retry. Cap at 5 attempts. This handles transient locks without overwhelming the platform on persistent contention.
// Pseudo-code for integration retry
async function updateWithRetry(record, attempt = 0) {
try { return await sf.update(record); }
catch (e) {
if (e.errorCode === 'UNABLE_TO_LOCK_ROW' && attempt < 5) {
const backoff = 200 * (2 ** attempt) + Math.random() * 100;
await sleep(backoff);
return updateWithRetry(record, attempt + 1);
}
throw e;
}
}Add static set guard to prevent trigger recursion
In Apex triggers that may recurse, use a static Set<Id> to track records already processed in the current transaction. Skip already-processed records to break the recursion loop.
public class AccountTriggerHelper {
private static Set<Id> processedIds = new Set<Id>();
public static void processAccounts(List<Account> accs) {
List<Account> toProcess = new List<Account>();
for (Account a : accs) {
if (!processedIds.contains(a.Id)) {
processedIds.add(a.Id);
toProcess.add(a);
}
}
// do work on toProcess only
}
}Sort child records by parent ID in bulk operations
For bulk updates of child records, sort the input by parent ID before submitting. This keeps all children of one parent in the same batch (in parallel mode), reducing cross-batch parent-lock contention dramatically.
Set up lock failure rate alerting
Track UNABLE_TO_LOCK_ROW occurrence rate in your integration logs. A baseline of <0.5% is normal; sustained >2% indicates a structural problem (parallel mode on shared parents, missing FOR UPDATE, etc.) that needs the structural fix, not just more retries.
Let BuildForce diagnose and fix this automatically
Instead of following manual steps, connect your org and let our AI identify exactly what's broken and how to fix it — in minutes.
Book a DemoCommon Questions
More answers about this issue and how to resolve it.
Stop debugging manually. Let AI do it.
BuildForce runs 200+ automated checks across your Salesforce org and tells you exactly what's broken and how to fix it.