- Fix Guides
- How to Fix Salesforce Bulk API Job Failures
How to Fix Salesforce Bulk API Job Failures
Step-by-step fix guide with AI-powered diagnosis from BuildForce.
Salesforce Bulk API 2.0 jobs fail when the job state hits 'Failed' (a fatal error like malformed CSV header) or when individual records fail with row-level errors (lock contention, validation rules, FLS). The fix path: switch parent-child loads to Serial mode to eliminate UNABLE_TO_LOCK_ROW on the same parent, tune batch sizes to the 10,000 record / 150 MB sweet spot (smaller batches retry faster), use the /jobs/ingest/{jobId}/failedResults endpoint to download row-level failures, and re-submit failed rows in a follow-up job instead of retrying the entire load.
Symptoms
Ingest job state showing 'Failed' with error 'CSV file is malformed'
Jobs completing with state 'JobComplete' but 30%+ records in failedResults
UNABLE_TO_LOCK_ROW errors on records sharing the same parent account
Job stuck in 'UploadComplete' state and never transitioning to 'InProgress'
Bulk delete jobs returning ENTITY_IS_DELETED for already-deleted records
Bulk API job slower than equivalent REST API calls for the same volume
Root Causes
Parallel mode causing parent-row lock contention
By default, Bulk API 2.0 processes batches in parallel. When multiple child records reference the same parent (e.g., 50 Opportunities on one Account), parallel threads attempt to lock the same parent row simultaneously, throwing UNABLE_TO_LOCK_ROW. Setting lineEnding=LF and processing in serial mode resolves this.
CSV header mismatch with sObject schema
Bulk API requires exact API-name column headers. 'Account Name' (label) instead of 'Name' (API name), or a missing required header for an external ID upsert, fails the entire job at upload time before any records are processed.
External ID field not properly indexed
Upsert operations on a custom external ID field that isn't marked as 'External ID' or doesn't have a unique index run extremely slowly and time out. Mark the field as External ID and unique before using it in upsert operations.
Validation rules or triggers throwing on bulk volume
Validation rules or Apex triggers that work fine for individual records can fail in bulk if they query the database without bulkification (SOQL inside a loop hits the 100-query governor at batch size >100).
Job state stuck because uploadComplete was never PATCHed
Bulk API 2.0 requires a PATCH to /jobs/ingest/{jobId} with state=UploadComplete after the CSV is uploaded. Forgetting this step leaves the job in 'Open' state indefinitely.
How to Fix It — Step by Step
Inspect the job's failedResults to find row-level errors
Download the failed results CSV — it shows exactly which rows failed and why. This is the only reliable way to distinguish data-quality errors from system errors.
GET /services/data/v59.0/jobs/ingest/{jobId}/failedResults
// Returns CSV with sf__Id, sf__Error, and the original row dataCheck the job state and transitions
Job state follows: Open → UploadComplete → InProgress → JobComplete (or Failed/Aborted). If a job is stuck in 'Open' or 'UploadComplete', you missed a PATCH or the platform is queuing.
GET /services/data/v59.0/jobs/ingest/{jobId}
// Look at 'state', 'numberRecordsProcessed', 'numberRecordsFailed'Switch to Serial mode for lock-contended loads
If failures are dominated by UNABLE_TO_LOCK_ROW, create the job with lineEnding=LF and concurrencyMode=Serial. Serial processes batches one at a time, eliminating parent-row contention at the cost of throughput.
POST /services/data/v59.0/jobs/ingest
{
"object": "Opportunity",
"operation": "insert",
"lineEnding": "LF",
"contentType": "CSV"
}
// Then PATCH to set concurrencyMode if needed (legacy Bulk v1)Tune batch size by record complexity
Bulk 2.0 auto-batches but you control the upload chunk size. Use 5,000 records per upload for objects with many triggers, 10,000 for vanilla inserts. Each batch must be ≤150 MB and ≤10,000 records.
PATCH the job to UploadComplete after uploading CSV
After the PUT of the CSV body, you MUST PATCH the job state to UploadComplete or Salesforce will not start processing. This is the most common reason for a 'job that never ran'.
PATCH /services/data/v59.0/jobs/ingest/{jobId}
{ "state": "UploadComplete" }Re-submit only the failed rows in a follow-up job
Take the failedResults CSV, strip the sf__Error column, and submit it as a new Bulk job. Don't retry the entire load — successful rows would be processed twice, doubling sObject row counts and triggers.
Verify external ID field metadata before upsert jobs
Query the field's IsExternalId attribute via Tooling API. If false, set 'External ID' in Setup → Object Manager → Field, otherwise upsert performance degrades exponentially with record count.
SELECT DeveloperName, IsExternalId, IsUnique FROM CustomField WHERE TableEnumOrId = 'Account' AND DeveloperName = 'External_ID'Let BuildForce diagnose and fix this automatically
Instead of following manual steps, connect your org and let our AI identify exactly what's broken and how to fix it — in minutes.
Book a DemoCommon Questions
More answers about this issue and how to resolve it.
Stop debugging manually. Let AI do it.
BuildForce runs 200+ automated checks across your Salesforce org and tells you exactly what's broken and how to fix it.