Skip to main content

Salesforce Concurrency & Record Locking: Resolving UNABLE_TO_LOCK_ROW Errors

💬 In plain words: The UNABLE_TO_LOCK_ROW error occurs when two concurrent transactions attempt to edit the exact same record or parent record simultaneously. To resolve it: sort data loads by parent ID to prevent interleaving, reduce batch chunk sizes, run processing in Serial mode for contested objects, and eliminate data skew on mega-parent records.
📌 Quick Example: Two parallel Bulk API batches are inserting Contact records that share a single parent Account ("Global Enterprises"). Each Contact insert briefly locks the parent Account. Because both batches hit the parent simultaneously, one times out with UNABLE_TO_LOCK_ROW. Sorting the input file by AccountId ensures each batch processes its own parent without overlapping.
🎬 Real-Life Case Study: The Batch Job That Fought the Dispatchers
  • The Scenario: A nightly scheduled Apex batch recalculates delivery pricing while evening shift dispatchers actively update delivery routes in the UI. The batch routinely fails with intermittent UNABLE_TO_LOCK_ROW errors.
  • The Anti-Pattern: Wrapping DML operations inside infinite retry loops or inflating batch sizes to 2,000 records. Updating child items locks the master Route__c record. A 2,000-record batch chunk holds locks on dozens of parent routes simultaneously, worsening resource contention.
  • The Technical Cause: Row locking is not a random glitch—it is structured database queuing. Blind retries during peak hours simply add more traffic to a backed-up queue.
  • The Solution Strategy:
    1. Order the batch query locator by parent ID (ORDER BY Route__c) so each chunk touches the fewest possible parents.
    2. Shrink the batch execution scope (e.g., from 2,000 down to 200).
    3. Reschedule heavy data updates outside active dispatcher working hours.
    4. When read-modify-write safety is required in code, explicitly invoke SELECT ... FOR UPDATE and keep execution units tight.
  • The Takeaway: Treat locks as queues rather than bugs. Design your architecture so concurrent processes rarely request the same parent record at the same time.

Understanding Record Locking & Concurrency

Record locking is Salesforce's built-in mechanism to prevent data corruption and lost updates during concurrent operations:

  • When an operation modifies a record, Salesforce locks that row (and frequently its parent or lookup targets) until the transaction completes.
  • If a secondary process attempts to edit a locked record, it enters a waiting state for approximately 10 seconds.
  • If the initial transaction does not release its lock within that 10-second window, the secondary process throws an UNABLE_TO_LOCK_ROW exception.
  • Common triggers include parallel Bulk API loads, automated triggers, and roll-up summary calculations firing against shared parent records.
  • Modifying or inserting multiple child records linked via Master-Detail or lookup relationships repeatedly locks the root master record.
  • Simultaneous batch jobs updating overlapping sets of parent records will experience deadlocks.
  • Explicit row locking can be managed programmatically using SOQL FOR UPDATE queries.
  • Using FOR UPDATE reserves queried rows for the duration of the execution context, ensuring safe read-modify-write patterns.
  • Mitigation techniques prioritize minimizing resource contention across active transactions.
  • Organize data loads so records referencing the same parent ID are grouped into single batches rather than spread across concurrent worker threads.
  • Reduce batch execution sizes to shorten the duration locks are held by any single transaction.
  • Switch Bulk API jobs from Parallel mode to Serial mode when processing highly contested objects.
  • Sequence background processes so conflicting automated jobs never process shared target parents at the same time.
  • Maintain short execution contexts and never execute callouts while holding active database locks.
  • Address ownership and lookup data skew, where an excessive number of child records (e.g., >10,000) point to a single parent record.
Record Locking Mechanics
├─ Update initiated → Locks record & parent targets
├─ Concurrent process requests same record → Waits ~10 seconds
├─ Lock timeout reached → Throws UNABLE_TO_LOCK_ROW
├─ Contention Sources: Data Skew, Parallel Bulk Jobs, Roll-Up Automation
└─ Fix Mnemonic 'GSSS': Group by Parent | Shrink Batch | Serial Mode | Sequence Jobs
🧠 Lock Fix Mnemonic — "GSSS": Group children by parent ID · Shrink batch size · Serial mode for data loads · Sequence conflicting jobs. The root cause is almost always multiple child updates contending for a single skewed parent.
🧭 360 Card — Record Locking Architecture
  • Rule: UNABLE_TO_LOCK_ROW indicates two concurrent transactions requested write access to the same record or its parent.
  • Gain: Row locks preserve data integrity and prevent lost updates during concurrent edits.
  • Price: Transaction throughput is reduced because secondary writers must queue and wait.
  • Limits: Lock acquisition timeout is capped at 10 seconds. SELECT ... FOR UPDATE maintains locks for the full transaction lifecycle.
  • The Alternative Risk: Disabling or ignoring concurrency controls causes read-modify-write race conditions that overwrite real data.
  • At Scale: Group child data by parent ID, reduce transaction chunk sizes, execute contested Bulk API loads in Serial mode, and remediate skewed parent records.
⚠ Developer Trap: You cannot execute HTTP callouts while holding active database locks. Salesforce enforces an unhandled exception if an external callout is attempted after issuing a FOR UPDATE query or performing DML. Always execute callouts before initiating database locks.

Core Q&A

Q: A nightly Bulk API load of Contact records intermittently fails with UNABLE_TO_LOCK_ROW. How do you resolve this without redesigning the integration middleware?
🎯 Say this first: This is parent-lock contention. Sort the payload by AccountId, reduce the batch chunk size, switch to Serial mode if necessary, and rebalance skewed parent Accounts.

The failure occurs because a transaction could not acquire a record lock within the 10-second timeout window due to an active lock held by another thread:

  • During Contact updates or inserts, the parent Account record is locked to maintain relationship integrity and recalculate summary fields.
  • When multiple Contacts referencing the same Account process in parallel Bulk API batches, those batches contend for the parent Account lock.
  • Account data skew (where a single Account owns tens of thousands of child Contacts) severely exacerbates lock contention.
  • Non-Invasive Remediation Steps:
    1. Sort the input CSV by AccountId prior to submission. This ensures Contacts sharing a parent are grouped into the same batch context rather than competing across parallel batches.
    2. Reduce batch sizes (e.g., from 10,000 records to 200–500) to ensure individual transactions complete and release locks faster.
    3. If locking persists, configure the Bulk API job to process in Serial mode instead of Parallel mode to eliminate concurrent batch evaluation entirely.
  • For severe skew (such as generic placeholder Accounts), distribute child records across multiple dummy parent Accounts to reduce thread locking.

Scenario-Based Technical Follow-ups

Q1: How does SOQL SELECT ... FOR UPDATE relate to row locking—is it a preventive tool or a failure cause?

Answer: It serves both roles depending on implementation accuracy.

  • FOR UPDATE is an explicit locking mechanism.
  • It locks selected records for the entire transaction lifecycle, preventing competing transactions from performing read-modify-write operations against the same records.
  • However, overusing FOR UPDATE or holding transactions open too long increases thread contention, potentially causing UNABLE_TO_LOCK_ROW errors in other processes.
  • Salesforce explicitly prohibits performing external HTTP callouts while holding active locks.
  • Best Practice: Keep FOR UPDATE query results narrowly scoped, place them as close to the DML operation as possible, and never perform web service callouts within the same execution context.
Q2: Two asynchronous batch jobs update overlapping sets of parent Accounts and frequently deadlock. What is the structural fix?

Answer: Serialize write access to the shared parent records.

  • Combine the separate operations into a single consolidated batch job, or partition data sets so the jobs never target identical parent Accounts simultaneously.
  • Schedule the jobs sequentially using System.schedule or chain them using Queueable Apex.
  • Running operations in Serial mode removes parallel thread execution, preventing deadlocks at the cost of total processing time.
  • Structurally, recurring deadlocks indicate flawed data architecture—redesign transaction workflows so any given parent record is mutated by only one worker thread at a time.
📝 2-Minute Architecture Self-Check

Q1: A high-volume data load throws UNABLE_TO_LOCK_ROW. What is the primary cause?
A1: Multiple concurrent batch chunks are attempting to lock the same parent record simultaneously. Resolve this by sorting input records by parent ID, reducing batch size, or utilizing Serial mode.

Q2: Why doesn't a user see explicit share records (Object__Share) for certain records they can access?
A2: Access may be granted via implicit sharing, role hierarchy inheritance, or Group membership. These access paths are dynamically computed by the system rather than stored as explicit share rows.