Performance & Scalability
UNABLE_TO_LOCK_ROW is an architecture warning—not a retry instruction
The exception is the symptom
A Salesforce transaction fails with UNABLE_TO_LOCK_ROW, so the immediate response is often to retry it. That is a poor default for a repeated production pattern. The exception says two transactions needed incompatible access to the same locked record at the same time; it does not say the failed transaction merely needed more persistence.
Salesforce uses record locks to preserve data integrity while transactions update related data. Failures emerge when concurrency converges on the same records or transactions hold locks long enough for other work to collide. The architectural task is to find the shared resource and reduce the frequency or duration of contention before adding recovery behaviour.
Draw the complete lock footprint
The record named in an error is not necessarily the only record involved. Salesforce's locking guidance is additive: object-specific behaviour, relationships, roll-up summaries and sharing operations can all extend a transaction's lock footprint. In the platform's published example, inserting an Opportunity can lock its Account; a roll-up summary locks its master; and a lookup can lock the referenced record as well.
Map every write path from the business event through Flow, Apex, managed packages and downstream updates. Include parents reached through relationships, records updated by automation, ownership changes and sharing recalculation. Then compare footprints across workloads that overlap. This turns a generic row-lock error into a concrete contention graph showing which operations compete on which records.
Hot parents are a data-model decision
Lookup skew occurs when many records reference the same lookup record. Each insert or update may need to lock that target to maintain referential integrity, so concurrent work on otherwise unrelated children can collide on one parent. Salesforce Well-Architected treats more than 10,000 children under a parent as a design signal rather than a hard platform limit and recommends distributing load where skew creates a hotspot.
Warning shapes include a universal holding Account, a default record referenced by most transactions, one queue or user owning enormous volume, and routing records used by every job. Ask whether the relationship is required. A picklist may model a stable classification better than a lookup; a nullable relationship may be safer than a catch-all parent. Where the parent is real, partition it by a meaningful business key.
Shorten the transaction before tuning concurrency
A transaction holds locks until its work completes. Extra queries, repeated calculations, synchronous callouts, duplicated automation and broad downstream updates extend the collision window. Profile the full save, not only the DML statement that finally reports failure. A fast update preceded by expensive orchestration can still be the point where a long-lived transaction exposes itself.
Move non-critical work beyond the commit boundary when the process permits it, while preserving ordering and failure semantics. Avoid having Flow and Apex independently recalculate the same parent. Narrow updates, remove redundant queries and keep integrations outside a database transaction unless atomicity requires them. Asynchronous processing helps only when jobs do not all return to the same hot record.
Partition work by the records it locks
Parallelism is valuable when work is independent. Salesforce's Bulk API guidance warns that parallel batches can contend for locks and recommends organising records by parent ID so references to the same parent stay in one batch. This preserves concurrency across different parents while serialising only the work that shares a lock domain.
Apply the same principle beyond data loads. Route messages, queueable work or scheduled processing using a stable key such as Account, policy, household or inventory item. Ensure one logical partition is processed in order while separate partitions can proceed concurrently. Do not choose a partition key merely because it is available; measure its distribution. A key that sends half the estate to one partition recreates the hotspot in a different layer.
Use serial mode as a controlled trade-off
Bulk API supports parallel and serial concurrency. Parallel mode is faster but can create contention; serial mode reduces that risk by processing batches one at a time and therefore increases elapsed time. Salesforce recommends reorganising batches first and using serial processing when parallel execution still produces lock timeouts that cannot be avoided.
Treat serial mode as an explicit throughput-for-reliability decision, not a permanent switch applied after one failed load. Record the job type, affected objects, batch ordering, expected volume and completion window. Remember that other parallel jobs can still overlap with a serial job. Coordinate large loads, ownership changes and business automation so a supposedly controlled run is not competing with daytime integrations or another migration.
FOR UPDATE protects invariants; it does not cool hotspots
Apex supports SELECT ... FOR UPDATE to lock queried records and prevent race conditions while a transaction validates and changes shared state. It is useful when correctness depends on reading a value and ensuring another transaction cannot alter it before the update—for example, reserving limited capacity or advancing a sequence under a defined invariant.
Adding FOR UPDATE to every contested query usually makes concurrency more explicit without removing the contention. Keep the locked scope small, make the protected section fast and lock records in a consistent order when several are required. If many requests legitimately need the same singleton record, redesign the coordination mechanism or partition the state. A lock can enforce exclusivity; it cannot manufacture throughput.
Retry only work that is safe to repeat
Bounded retries with increasing delay and jitter can recover from genuinely transient contention. They should have a strict attempt limit, a fresh transaction for each attempt and observable exhaustion. Never retry blindly inside a tight loop: that raises pressure on the same hotspot and consumes limits while the competing transaction may still hold the lock.
Before retrying, make the operation idempotent. Use a stable request key, detect prior completion and separate database state from external side effects such as payments, emails and events. A transaction that partially affected another system before failing cannot simply be replayed safely. Capture the lock target, operation, attempt, latency and outcome so retries become evidence for diagnosis rather than a mechanism that hides a deteriorating design.
Treat sharing changes as their own workload
Salesforce documents an organisation-wide group membership lock for changes involving role and territory hierarchies, groups and queues. Those operations are different from ordinary row updates and can block one another across a wide scope. Large ownership moves or frequent group maintenance should therefore be planned and sequenced, especially when private sharing models require substantial recalculation.
Separate access-model administration from high-volume transactional processing where possible. Test hierarchy and ownership changes with production-like volume, schedule them deliberately and confirm their completion before starting dependent work. If one integration user or queue owns a disproportionate share of an object, address ownership skew as part of the data and access architecture rather than accepting slow recalculation as an administrative inconvenience.
The JSBC Labs view
Recurring row-lock errors are an architecture signal with four usual levers: reduce the number of transactions that need the same record, shorten how long each transaction holds locks, partition concurrent work by lock domain and make unavoidable retries safe and observable. Start with the lock footprint and data distribution before changing batch size or adding delay.
The practical success measure is not that the exception disappears from a dashboard. It is that business-critical work completes predictably at expected concurrency, without serialising the entire platform or concealing failures behind endless retries. Design for independent records to remain independent. When shared state is unavoidable, make its ownership, ordering and contention budget explicit.
Official references
- Salesforce Engineering: Platform Record Locking and Concurrency
- Salesforce Engineering: Managing Lookup Skew to Avoid Record Lock Exceptions
- Salesforce Well-Architected: Resource and Cost Optimization
- Salesforce Bulk API Developer Guide: General Guidelines for Data Loads
- Salesforce Apex Developer Guide: Locking Statements
- Salesforce Well-Architected: Reliability