Fix Guide

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.

Parent LockSerial ModeFOR UPDATERetry with Jitter

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

1

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.

2

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.

3

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.

4

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.

5

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

1

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.

2

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).

3

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.

Example
// Inventory decrement pattern
Account acc = [SELECT Id, Inventory_Count__c FROM Account WHERE Id = :accountId FOR UPDATE];
acc.Inventory_Count__c -= 1;
update acc;
4

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.

Example
// 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;
  }
}
5

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.

Example
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
  }
}
6

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.

7

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 Demo

Common 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.