Bulk API import for contact management fails with row lock errors during high-volume load

Running a bulk contact import using Data Loader with 50,000 records. The job starts fine but after processing about 15,000 records, we start seeing UNABLE_TO_LOCK_ROW errors for random contacts. We’re using Bulk API 2.0 with parallel mode enabled (batch size 10,000). The contacts being imported have relationships to accounts and opportunities.

Error pattern:


UNABLE_TO_LOCK_ROW: unable to obtain exclusive access
Record locked by: automated process
Operation: UPDATE Contact

We have workflow rules and process builders active on the Contact object, plus a trigger that updates related opportunity stages. The import causes data inconsistency because some contacts update successfully while others fail. Is there a way to handle Bulk API concurrency better, or do we need to disable automations during import?

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:

  1. Refactor trigger to bulkify opportunity updates (eliminates most locks)
  2. Switch to Serial mode for this import (ensures no parallel batch conflicts)
  3. Reduce batch size to 5,000 (faster failure recovery if locks still occur)
  4. Use Database.update(records, false) to allow partial success
  5. 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.

Row locking in Bulk API happens when multiple batches try to update the same or related records simultaneously. With parallel mode, you’re processing multiple batches at once, which increases lock contention. Try switching to serial mode by setting the concurrencyMode to ‘Serial’ in your job creation. This processes batches one at a time, eliminating most lock conflicts. It’s slower but much more reliable for related data imports.

Those automated process locks are definitely from your workflow rules or process builders. When you bulk import, each contact triggers automation that might update the account or opportunity. If another batch is simultaneously updating a related account, you get the lock. I’d recommend creating a custom checkbox field like ‘Bulk_Import_Mode__c’, set it to true before import, and add that as a filter criteria to your workflows and process builders to skip them during bulk operations.

Switched to Serial concurrencyMode with 3,000-record batches in our Bulk API v2 job creation payload and row lock errors dropped from 847 to zero overnight.

Good points. I checked and we have 3 workflow rules and 2 process builders on Contact that update Account fields and create Tasks. The trigger also does opportunity stage updates based on contact role. Switching to serial mode would work but we’re trying to meet a tight migration deadline. Is there a way to keep parallel mode but reduce lock contention? Maybe smaller batch sizes?

Smaller batches help but don’t solve the root cause. If your trigger updates opportunities, and multiple contacts are related to the same opportunity, you’ll still get locks. Consider refactoring your trigger to collect all opportunity IDs and do a single bulk update at the end instead of updating each one individually. Also, check if your workflow rules have time-dependent actions - those can hold locks longer than expected.

The trigger architecture matters here. Are you bulkifying your opportunity updates? If your trigger has a loop that does individual DML operations, that’s causing the locks. You need to collect all opportunities to update in a Map or Set, then do a single update operation outside the loop. Also consider using Database.update with allOrNone=false to allow partial success instead of failing the entire batch on lock errors.