You’ve got a classic bulk import concurrency problem with multiple contributing factors. Let me address all three key areas:
1. Bulk API Concurrency Modes:
You’re using Parallel mode which processes up to 10 batches simultaneously. This maximizes throughput but creates lock contention when records share relationships. Your options:
- Serial Mode: Set concurrencyMode=‘Serial’ in job creation. Processes one batch at a time. Slower but eliminates 90% of locks.
- Parallel with smaller batches: Reduce from 10,000 to 2,000-5,000 records per batch. Fewer records per batch = less chance of relationship overlap = fewer locks.
- Hybrid approach: Use Serial mode for the initial import, then Parallel for subsequent incremental loads when you’re updating fewer records.
2. Row Locking in Salesforce (Root Cause):
The ‘automated process’ lock means workflows/process builders/triggers are executing and holding locks on parent/child records. When Contact A and Contact B both update the same Account or Opportunity, the second update waits for the first to complete. In Bulk API, if that wait exceeds the timeout, you get UNABLE_TO_LOCK_ROW.
Implement lock-aware retry logic:
try {
update opportunities;
} catch (DmlException e) {
// Retry lock failures after delay
}
3. Trigger/Workflow Side Effects (Critical Fix):
Your trigger updating opportunity stages is the main culprit. Here’s what to fix:
Trigger Refactoring - Must be bulkified:
Map<Id, Opportunity> oppsToUpdate = new Map<Id, Opportunity>();
for(Contact c : Trigger.new) {
if(c.OpportunityId__c != null) {
oppsToUpdate.put(c.OpportunityId__c,
new Opportunity(Id=c.OpportunityId__c, StageName='Contacted'));
}
}
if(!oppsToUpdate.isEmpty()) {
Database.update(oppsToUpdate.values(), false);
}
Workflow/Process Builder Control:
Create a custom setting or field to bypass automation during bulk import:
- Add Custom Metadata: Bulk_Import_Settings__mdt with Active__c checkbox
- Modify workflow criteria: AND(NOT($Setup.Bulk_Import_Settings__mdt.Active__c))
- In trigger: if(Bulk_Import_Settings__mdt.getInstance(‘Default’).Active__c) return;
Recommended Solution:
- Refactor trigger to bulkify opportunity updates (eliminates most locks)
- Switch to Serial mode for this import (ensures no parallel batch conflicts)
- Reduce batch size to 5,000 (faster failure recovery if locks still occur)
- Use Database.update(records, false) to allow partial success
- Process failed records in a second pass with Serial mode
For Future Imports:
Consider using Bulk API 2.0’s built-in retry mechanism by setting your batch size smaller and letting Salesforce handle retries automatically. Or implement a custom retry queue that reprocesses UNABLE_TO_LOCK_ROW failures after a delay.
The data inconsistency you’re seeing is because Bulk API processes in all-or-nothing batches. Failed batches roll back, but successful batches commit. With allOrNone=false, you get partial success which is usually better for bulk imports where some locks are unavoidable.
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.