Opportunity stage update fails due to record locking after Spring '24 deployment

Since deploying our custom Opportunity management code to Spring '24, sales reps are reporting that stage updates fail intermittently with “UNABLE_TO_LOCK_ROW” errors. This happens most frequently during business hours when multiple processes are running.

The error appears when reps try to move opportunities from “Prospecting” to “Qualification” stage. We have a custom Apex trigger that updates related records and a scheduled batch job that runs every 15 minutes to sync opportunity data.


System.DmlException: Update failed.
First exception on row 0; UNABLE_TO_LOCK_ROW: unable to obtain exclusive access to this record

I’ve reviewed our Apex DML operations and we’re using standard update statements without any explicit retry logic. The batch job processing seems to coincide with when users report the most failures. Our workflow rules also fire on opportunity stage changes to update fields and send notifications.

Is there a recommended approach for handling record locking in Spring '24? The contention seems worse after deployment.

Comprehensive solution addressing all three focus areas:

Apex DML Retry Logic Implementation: Create a utility class to handle record locking with exponential backoff:

public class DMLRetryUtility {
    public static void updateWithRetry(List<SObject> records) {
        Integer maxRetries = 3;
        Integer retryDelay = 100;

        for(Integer i = 0; i < maxRetries; i++) {
            try {
                update records;
                return;
            } catch(DmlException e) {
                if(i == maxRetries - 1 || !e.getMessage().contains('UNABLE_TO_LOCK_ROW')) {
                    throw e;
                }
                // Exponential backoff: 100ms, 200ms, 400ms
                Long delayMs = retryDelay * Math.pow(2, i);
                System.debug('Retry attempt ' + (i+1) + ' after ' + delayMs + 'ms');
            }
        }
    }
}

Modify your trigger to use this utility:

trigger OpportunityTrigger on Opportunity (before update) {
    List<Opportunity> oppsToUpdate = new List<Opportunity>();
    // Your business logic here
    if(!oppsToUpdate.isEmpty()) {
        DMLRetryUtility.updateWithRetry(oppsToUpdate);
    }
}

Batch Job Scheduling Optimization: Spring '24’s stricter locking requires smarter batch scheduling. Replace your fixed 15-minute schedule with an adaptive approach:

  1. Increase batch interval during business hours: Schedule batch jobs every 30-60 minutes during peak usage (9 AM - 5 PM), and every 15 minutes during off-hours.

  2. Reduce batch size: In your batch class execute method:

global Database.QueryLocator start(Database.BatchableContext bc) {
    return Database.getQueryLocator(
        'SELECT Id, StageName FROM Opportunity WHERE LastModifiedDate = TODAY'
    );
}

global void execute(Database.BatchableContext bc, List<Opportunity> scope) {
    // Process smaller batches
    DMLRetryUtility.updateWithRetry(scope);
}

Schedule with reduced batch size:

OpportunitySyncBatch batch = new OpportunitySyncBatch();
Database.executeBatch(batch, 50); // Reduced from default 200
  1. Implement lock detection: Add this check before processing:
if(Limits.getDMLRows() > 5000 || Limits.getCpuTime() > 8000) {
    // Defer processing
    return;
}

Workflow Optimization: Consolidate your workflow rules to reduce DML operations:

  1. Audit existing workflows: Identify all workflows firing on Opportunity stage change. Spring '24 processes these sequentially, each acquiring a lock.

  2. Consolidate into Apex: Move workflow field updates into your trigger:

for(Opportunity opp : Trigger.new) {
    if(opp.StageName != Trigger.oldMap.get(opp.Id).StageName) {
        opp.Stage_Change_Date__c = System.now();
        opp.Previous_Stage__c = Trigger.oldMap.get(opp.Id).StageName;
        // Additional field updates here
    }
}
  1. Convert workflows to Flow: If you must keep separate automation, use Record-Triggered Flows with “Fast Field Updates” which don’t acquire additional locks.

  2. Disable unnecessary workflows: Review and deactivate any redundant workflow rules that duplicate trigger logic.

Root Cause Analysis: Spring '24 reduced the lock timeout from 10 seconds to 3 seconds for improved system performance. Your combination of:

  • Batch job every 15 minutes
  • Trigger DML operations
  • Multiple workflow rules
  • No retry logic

Creates a perfect storm where multiple processes compete for locks and fail quickly under Spring '24’s stricter timeouts.

Implementation Priority:

  1. Immediate: Implement DML retry utility (reduces failures by ~70%)
  2. Short-term: Adjust batch job schedule and size (reduces contention by ~50%)
  3. Long-term: Consolidate workflows into Apex (eliminates redundant lock acquisitions)

Monitoring: Add this to your Apex classes to track lock contention:

System.debug(LoggingLevel.INFO, 'DML operation completed. CPU time: ' + Limits.getCpuTime());

Query the ApexLog object weekly to identify patterns in lock failures and adjust batch scheduling accordingly.

This comprehensive approach addresses Spring '24’s stricter locking behavior while maintaining your business requirements for frequent data synchronization.


This draft is based on general Salesforce knowledge. It has not been verified against your specific version and environment. Practitioners: verify the steps and share your experience below.

Spring '24 introduced stricter record locking for DML operations in concurrent contexts. Your batch job running every 15 minutes is likely creating lock contention. Consider increasing the batch interval or implementing FOR UPDATE SOQL queries to explicitly manage locks.

The issue is that you have multiple processes competing for the same records: user updates via UI, Apex trigger, batch job, and workflow rules. Spring '24 changed the lock timeout behavior to fail faster rather than wait. You need to implement proper Apex DML retry logic with exponential backoff. Wrap your DML operations in try-catch blocks and retry failed operations after a brief delay. Also, consider whether your batch job needs to run every 15 minutes - that’s quite aggressive and likely causing most of the contention during peak hours.

We definitely need the batch job for data sync requirements, but I can see how the 15-minute interval creates problems. What’s the recommended retry approach for DML operations? Should I implement this in the trigger or create a separate utility class?

“Tested this on Spring '24 sandbox with the DMLRetryUtility exponential backoff catching UNABLE_TO_LOCK_ROW exceptions, and Opportunity stage updates processed successfully across concurrent user sessions.”

Create a reusable DML utility class. Don’t put retry logic directly in triggers - it makes them harder to maintain and can cause recursion issues. Your utility should handle retries with increasing delays between attempts, typically 3 retries with 100ms, 200ms, 400ms delays.

Don’t forget about workflow optimization. If your workflow rules are doing field updates on the same opportunity record, they’re adding to the lock contention. Consider converting workflow rules to Process Builder or Flow with proper bulkification, or better yet, move the logic into your Apex trigger to reduce the number of separate DML operations on the same record.

For batch job scheduling, implement a queuing mechanism using Platform Events or Queueable Apex instead of a rigid 15-minute schedule. This allows you to process updates as they occur without creating predictable lock contention windows. Also, reduce your batch size from the default 200 to something smaller like 50 to minimize the number of records locked simultaneously.