- 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:
- Order the batch query locator by parent ID (ORDER BY Route__c) so each chunk touches the fewest possible parents.
- Shrink the batch execution scope (e.g., from 2,000 down to 200).
- Reschedule heavy data updates outside active dispatcher working hours.
- 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
- 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.
Core Q&A
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:
- 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.
- Reduce batch sizes (e.g., from 10,000 records to 200–500) to ensure individual transactions complete and release locks faster.
- 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
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.
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.
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.