Skip to content

Parallel and Locking

Module 4 of 8 in the chapter Moving Data at Scale, chapter 10 of the Integration Architect course. Each module stands on its own. Previous: Two Docks: 2.0 Now, 1.0 for What Only It Does · Next: Bulk Query

The question this module answers: Bulk API 2.0 runs everything in parallel. When does that make a load fail, and what can your program do about it?

I ask this in every class. A bulk job can run in serial mode or parallel mode. Which one’s faster? Parallel, and the room always gets it. Then: why wouldn’t you always go parallel?

Record locking. Picture a plumbing supplier with hundreds of invoices, all under one account, blended into two files. Each file goes to its own batch, the two batches run at exactly the same moment, and both try to save invoices under that one account. Both need a lock on the account record. One gets it. The other waits, and if it waits too long, it fails.

And here’s the part that makes it dangerous: it could happen today and not happen tomorrow. It might not happen in your developer org, or in a small sandbox, and then happen in production, on the night you’re loading your most important data. That’s why locking is a design decision, not something you find out about later.

On Bulk API 2.0 you upload one CSV and Salesforce makes the batches (module 2). It runs them at the same time, and inside each batch it saves the records in groups: “each chunk of 200 records is processed as a separate transaction” (Limits (opens in a new tab), Bulk API 2.0 and Bulk API Developer Guide, Winter ‘27, checked 2026-11-01).

When a transaction saves a child record, Salesforce locks the parent it points to, for as long as that transaction runs. A lookup does it too, not only a master-detail. Another transaction that needs the same parent waits up to 10 seconds, then fails with UNABLE_TO_LOCK_ROW. The Data Architect course takes a lock apart in The Lock Footprint of a Save: what a save locks, and why automation makes the lock last longer.

On Bulk API 2.0, parallel isn’t a choice. The job reference says of its concurrencyMode field: “Currently only parallel mode is supported,” and if other modes arrive, “the mode will be chosen automatically by the API and will not be user configurable” (Create a Job (opens in a new tab), same guide, checked 2026-11-01). So on 2.0 there’s no switch to flip. Everything your program can do about locking, it does before the upload and after the results.

There are two places the locks come from. The first is inside your job: two batches that share a parent. The second is easy to forget: everything else writing to the same parents at the same moment.

Salesforce says it plainly in Bulk API 1.0’s job reference: “Batches from the same job, and from other jobs submitted with parallel mode, are processed simultaneously.” (JobInfo (opens in a new tab), same guide, checked 2026-11-01). Add the synchronous calls coming in through the teller window, the flows, the triggers, and a person saving a record in the browser. Every one of them that saves a child locks its parent.

Every writer locks the same parent Every writer locks the same parent, in two frames. Frame 1, inside the job: two batches of the migration job, each holding Work Orders under Redrock, run at the same moment against Redrock's account, highlighted as the parent. Batch 1 locks it; batch 2 waits, its arrow red with an X. Frame 2, outside the job: Redrock's live revisions, sent by the integration layer with Composite, arrive at the same moment and wait too, red with an X. Underneath: saving a child locks its parent for that transaction; count every writer in the window, not only your job. WRITING AT THE SAME MOMENT THE PARENT Migration job, batch 1 Work Orders under Redrock Migration job, batch 2 more Work Orders under Redrock Redrock's account the parent every one of them saves under locks waits Saving a child locks its parent for that transaction. Count every writer in the window, not only your job. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Inside the job: two batches, one parent. One locks, the other waits.

Picture Steve Trucking’s migration with no plan for the window. The RouteDesk load sends about 196,400 Work Orders through the loading dock (Data Architect, Loading at Volume), and every one belongs to a Retail Partner. Meanwhile Redrock doesn’t stop. Its revisions come through the integration layer, minutes after Redrock makes them, and they update the same Work Orders.

Every writer locks the same parent Every writer locks the same parent, in two frames. Frame 1, inside the job: two batches of the migration job, each holding Work Orders under Redrock, run at the same moment against Redrock's account, highlighted as the parent. Batch 1 locks it; batch 2 waits, its arrow red with an X. Frame 2, outside the job: Redrock's live revisions, sent by the integration layer with Composite, arrive at the same moment and wait too, red with an X. Underneath: saving a child locks its parent for that transaction; count every writer in the window, not only your job. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.57.1", "built": "2026-11-01", "id": "DGM-INT-BK-04", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.5.0", "theme": "stevetecharc 1.4.0"} WRITING AT THE SAME MOMENT THE PARENT Migration job, batch 1 Work Orders under Redrock Migration job, batch 2 more Work Orders under Redrock Redrock's live revisions the integration layer, sent with Composite Redrock's account the parent every one of them saves under locks waits waits Saving a child locks its parent for that transaction. Count every writer in the window, not only your job. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Outside the job: the live feed needs the same parent. Count every writer in the window.

This is the integration architect’s view of locking. The Data Architect sees one load. You see the landscape, so you’re the one who knows what else is running at 2 a.m. In my experience, the most common cause of lock errors is exactly this: parallel work arriving at the same time, each piece reasonable on its own.

The first lever is in the file. If the rows for one parent sit together, the batch that holds them works on that parent, and the other batches work on other parents. Salesforce gives the same advice: “When changing child records, group them by parent—group records by the field ParentId in the same batch to minimize locking conflicts” (Loading Data from the API (opens in a new tab), Best Practices for Deployments with Large Data Volumes, checked 2026-11-01).

Sort the load by parent Three batches of Work Orders loading in parallel on the left; on the right, their parents, the Retail Partner Accounts Keystone, Redrock and Brightwave. First, each batch holds Work Orders from all three partners, so all three need Redrock at the same moment: batch 2 locks it, and batches 1 and 3 wait, their arrows red with an X. Then the same work, sorted by partner, tagged "sorted": batch 1 holds only Keystone's Work Orders, batch 2 only Redrock's, batch 3 only Brightwave's, and each goes to its own parent. Underneath: batches that share parents wait on each other's locks; sort the file by parent, and each batch works on its own. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.46.0", "built": "2026-10-27", "id": "DGM-DA-MG-22", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.3.0", "theme": "stevetecharc 1.4.0"} BATCHES, IN PARALLEL PARENTS: THE RETAIL PARTNER ACCOUNTS Batch 1 Keystone's Work Orders only sorted Batch 2 Redrock's Work Orders only sorted Batch 3 Brightwave's Work Orders only sorted Redrock Retail Partner Account Keystone Retail Partner Account Brightwave Retail Partner Account its own parent its own parent its own parent Batches that share parents wait on each other's locks. Sort the file by parent, and each batch works on its own. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

From the Data Architect course: sort the file by parent, and each batch works on its own.

On 2.0 there’s one thing to keep in mind. Salesforce makes the batches, and the developer guide doesn’t say where it cuts the file. Sorting puts each parent’s rows next to each other. That’s what you control, and it’s what Salesforce asks for.

Sorting has a limit, though. If one parent has most of the children, its rows fill several batches, and those batches still meet at that one parent. That’s parent skew, and the fix for it is in the data model, not the file. The Data Architect course covers it in Hot Spots. Steve Trucking’s design already has the answer: each distribution center has its own account under its Retail Partner, so the Work Orders spread over nine parents, not three (Checkpoint 2). Sort by those nine accounts, and each batch has a smaller crowd to wait behind.

Your program won’t get an error the moment a lock is missed. Salesforce tries again first. The job’s retries field counts it: “The number of times that Salesforce attempted to save the results of an operation. The repeated attempts are due to a problem, such as a lock contention” (Get Job Info (opens in a new tab), same guide, checked 2026-11-01). If that number climbs, you’re running into locks, even on a night when nothing fails. The limits page adds: “If you receive a message that the API retried more than 20 times, use a smaller upload file and try again” (Bulk API and Bulk API 2.0 Limits and Allocations (opens in a new tab), checked 2026-11-01).

When the retries run out, records fail. The job can still finish, with some records in the failed results, or end in the Failed state, and “job data that was successfully processed isn’t rolled back.” Each row of the failed results carries an sf__Error column with the error code and message (Get Job Failed Record Results (opens in a new tab), same guide, checked 2026-11-01). Lock failures say UNABLE_TO_LOCK_ROW.

Nothing resends them for you. Salesforce’s job reference says it for parallel mode: “When record locking is severe, the job can fail. Any failed records must be resubmitted.” So your program has three things to do:

  1. Record the retries count for every job, every night, so you see the trend before the failures.
  2. Separate the lock failures from the rest. A lock failure is worth another try; a validation error isn’t, and sending it again just fails again. Module 6 reads the results file in full.
  3. Resubmit only the lock failures, in a new job, when the parents are quiet. Not the whole file, and not straight away into the same crowd.

A retry is a recovery, not a design. If the same feed needs one every night, find the overlap instead. The Data Architect course makes that case in Designing Out Contention.

When sorting by parent doesn’t stop the failures, these are the levers, in the order I’d reach for them:

  • Clear the window. Find every other writer to the same parents and move it, or hold it. For RouteDesk, the cutover plan does most of this for you. The 190,000 delivered Work Orders load on Day −7, a week early and hidden by sharing, while Redrock’s manifests still go to RouteDesk. Day −1 brings the week’s changes, and Day 0 is a small incremental. Only then do the partners’ manifests switch to Salesforce (Cutover Weekend). Get the copy into sync first, then turn the live feed on. If a revision still arrives inside the window, the integration layer holds it until the incremental is done. Either way, it’s decided before the weekend, not discovered during it.
  • Make each save lighter. Automation on the child makes every transaction longer, and a longer transaction holds the parent’s lock longer. Know what fires on each save. The Data Architect course calls this reducing the footprint.
  • Upload smaller files. It’s Salesforce’s own advice when the retries pile up, and it means fewer records competing at once.
  • Spread the parents. The data model fix above: more parents, fewer children waiting on each one.
  • Use serial mode, on Bulk API 1.0. Salesforce’s job reference: “If parallel mode results in too many failed records or failed jobs, submit the job with Serial concurrency mode. In this mode, batches from the job and batches from other serial mode jobs are processed one at a time” (JobInfo (opens in a new tab), checked 2026-11-01). It’s slower, and the same reference warns it “can significantly increase the processing time,” but the batches stop competing with each other. I use it when the data is complex and we’re hitting collisions and timeouts. Salesforce is more careful: serial mode “is used only when previously submitted asynchronous jobs have accidentally produced contention because of locks. Use only when advised by Salesforce” (Limits (opens in a new tab), checked 2026-11-01). It’s also only on the older dock, with the sign-in to plan from module 3, and it doesn’t stop other writers, only your own batches competing with each other.

So the order is: sort, clear the window, lighten, shrink, spread, and serial last. Speed against reliability is a judgment call, not a fixed rule. I don’t start with “always parallel” or “always serial.” I look at the shape of the data and what else is running, and I choose.

Locking hides from small tests. A developer org with a hundred records and nobody else working in it won’t show you anything. You need realistic volumes, the real automation, and the other writers running too. That means a full sandbox, with realistic data sizes, and a rehearsal that includes the live feeds. If the rehearsal is a performance or scale test, it needs Salesforce’s approval ahead of time; the Data Architect course has the details.

And reading is different from writing. A lock stops other writers, not readers. Keystone’s nightly status file and the weekly export to Snowflake are bulk queries (module 5): they read, so they don’t take these locks. The jobs to test for locking are the ones that save records.

Test knowledge. Know why parallel can fail: saving a child record locks its parent, so parallel batches that share parents wait on each other, and records fail with UNABLE_TO_LOCK_ROW. Know the defence: group the child records by parent (ParentId) so they travel in the same batch. Know that Bulk API 2.0 runs only in parallel, and that serial mode (Bulk API 1.0) processes one batch at a time, slower, with less lock contention; Salesforce keeps it for contention that grouping didn’t solve. Failed records aren’t resent for you; you resubmit them.

  • Which parents each bulk job writes under, and how many children each one gets. That’s where the locks will be.
  • What else writes to those parents in the same window: other jobs, the synchronous feeds, automation, people. Then whether each one is moved, held, or allowed to wait.
  • How the file is sorted before it’s uploaded, and whether a hot parent needs a change in the data model instead.
  • What the program does with the results: the retries count recorded every run, lock failures separated from the rest, and resubmitted only when the parents are quiet.
  • When a load goes to serial mode on Bulk API 1.0, and how that program signs in.
  • Where the rehearsal happens: a full sandbox, realistic volumes, the other writers running.