The Lock Footprint of a Save
Ta treść nie jest jeszcze dostępna w Twoim języku.

The question this module answers: What does saving one record actually lock?
The 4 p.m. Rush
Section titled “The 4 p.m. Rush”It’s four in the afternoon at Steve Trucking. Across nine distribution centers, 330 trucks are on their last few stops. A crew in one city taps Delivered in the Field Service app. A crew across town taps Installed. Another marks an exception: the Recipient wasn’t home. Each delivery goes through about eight status changes in a day, and with about 3,300 deliveries a day, that’s more than 26,000 saves, and a lot of them land in the same late-afternoon hour. At the same moment, a Retail Partner’s system is asking for status through its API, a dispatch coordinator is moving tomorrow’s stops around, and tomorrow’s revised manifest is arriving.
Every one of those saves looks like one record: one Work Order, one status. But each Work Order belongs to a Manifest, the partner’s list for that DC and that day, with a few hundred deliveries on it. And every Work Order points at its Retail Partner, and there are only three of them. So hundreds of crews, each saving a different record, can all be reaching for the same parent at the same moment.
That’s this module’s question. The user thinks, “I updated one record.” The architect needs to ask: what is the lock footprint of that save? Under concurrency, the size and shape of that footprint decide whether the design works at 4 p.m. or only at 10 a.m.
Why Do We Lock Anything?
Section titled “Why Do We Lock Anything?”Before Salesforce, let’s look at the problem locking solves. A delivery’s line count says 2. Two processes read it at the same moment: one adds a haul-away, one adds a part. Each writes back 3. The right answer is 4. Without protection, the second write quietly erases the first. That’s called a lost update, and it’s the reason every database locks something.
A lock gives one transaction temporary control of the records it needs, so another transaction can’t make a conflicting change at the same time. That protection is a good thing. The trouble starts when lots of transactions need the same locks at the same time.
Two Ways to Lock: Optimistic and Pessimistic
Section titled “Two Ways to Lock: Optimistic and Pessimistic”There are two broad ways a system can protect a record, and they start from opposite assumptions.
Optimistic locking assumes you won’t collide. Nothing is frozen while you work. You read the record, make your changes, and at save time the system checks whether anyone else changed the record since you read it. Usually nobody has, and the save goes through. If somebody has, the system catches it then, and your save is rejected so you can reload and try again. You don’t pay for protection you rarely need; you deal with the conflict when it actually happens.
Pessimistic locking assumes you will collide. So you take the lock before you start your change: lock, change, release. Nobody else can change that record until you’re done. Some systems take that lock the moment a user clicks Edit, and hold it while the edit screen is open. On the web that’s rarely practical. People open a record and go to lunch, close the browser tab, lose their connection, and the server never hears about it. The lock can be held for a long time, and you need a rule for when to release it: a timeout, or someone allowed to break the lock. Document-management systems do this on purpose with check-out and check-in. General record editing on the web almost never does.
So which one is Salesforce? Both, at two different levels.
- Across the user’s edit session, Salesforce is optimistic. Opening a record to edit it locks nothing. If someone else saves the record while you’re editing, Salesforce catches it when you save, with what it calls collision detection: “The record was modified by [user] during your edit session. Make a note of the data you entered, then reload the record and enter your updates again.”
- Inside the save itself, Salesforce is pessimistic, but brief. While the save’s transaction runs, the records it changes are locked, and so are related records: “When you perform a DML operation on one record, related records are locked in addition to the record in question.” The lock is released when the transaction completes. Another transaction that needs one of those records waits, up to 10 seconds, and then fails.
FOR UPDATEis pessimistic locking you ask for in Apex: lock these records now, for the rest of this transaction.
Integrations can be optimistic too, and they should be when they update a record that people also edit. The partner’s system reads a delivery at 9:00, and a coordinator changes the delivery window at 9:05. At 9:10 the partner sends its update, built from what it read at 9:00. Without a check, it quietly overwrites the coordinator’s change: the lost update again, this time across the API. Salesforce’s REST API lets the caller say “only if nobody has changed it since I read it”:
PATCH /services/data/v68.0/sobjects/WorkOrder/0WOxx0000000001If-Unmodified-Since: Fri, 30 Oct 2026 09:00:00 GMTContent-Type: application/json
{ "Status": "Rescheduled" }REST API “processes the request only if the data hasn’t changed since the specified date. Otherwise, a 412 Precondition Failed status code is returned, and the request isn’t processed.” The caller then reads the record again and decides, which is exactly what Salesforce asks of a user who hits collision detection. Two details from the same page: this is “weak validation,” compared to the record’s last modified date, and the stronger If-Match check with ETags works on sObject Rows for “Account records only” (Conditional Request Headers (opens in a new tab), REST API Developer Guide, checked 2026-10-31).
Optimistic at the user level, pessimistic at the transaction level, and hopefully that transaction lasts about a second. That’s why, when I need to be sure nobody changes a record while my code decides what to do with it, I query it FOR UPDATE, get my lock, make my changes, and let the transaction finish. And I usually lock one record, not fifty thousand. A lock is protection, but it’s also a line: everyone else who needs that record waits behind you.
Test knowledge. Know what Salesforce locks and for how long: the records a save changes and related records, held until the transaction completes; a transaction waiting on a lock waits up to 10 seconds, then fails: a locking query throws a
QueryException, a DML statement aDmlExceptionwith the status codeUNABLE_TO_LOCK_ROW.FOR UPDATElocks queried records explicitly, can’t be combined withORDER BY, and its locks are released if the transaction makes a callout. Other users can still read a locked record, so a read-then-write withoutFOR UPDATEcan overwrite someone else’s change.
What FOR UPDATE Looks Like
Section titled “What FOR UPDATE Looks Like”Here’s the lost update from the start of this module, at Steve Trucking. Say the Manifest keeps a count of its delivered Work Orders in a field, Delivered_Count__c, and a little Apex adds to it each time a crew taps Delivered. (A roll-up summary would do this for us on a master-detail. The Manifest is a lookup, so it’s coded, which is common when a team has moved off master-detail for exactly the reasons in this chapter.)
Two crews tap Delivered in the same second. Both transactions read the Manifest: 211 delivered. Both add one. Both write 212. The right answer is 213. Locked records can still be read, which is why: Salesforce’s own guide warns that a second update can “overwrite changes made by the locking client if the second client obtained an old copy of the record,” and gives the fix: “the second client must lock the record first. The locking process returns a fresh copy of the record” (Locking Statements (opens in a new tab), Apex Developer Guide, checked 2026-10-31).
So we read the Manifests FOR UPDATE:
public with sharing class ManifestDeliveredCount { // Called from the Work Order trigger: for each Manifest, how many of its // Work Orders just became Delivered in this transaction public static void add(Map<Id, Integer> deliveredByManifest) { // Lock the Manifests and read a fresh copy of each. // Anyone else who needs them now waits until this transaction completes. List<Manifest__c> manifests = [ SELECT Id, Delivered_Count__c FROM Manifest__c WHERE Id IN :deliveredByManifest.keySet() FOR UPDATE // a locking query can't use ORDER BY ];
for (Manifest__c manifest : manifests) { Decimal current = manifest.Delivered_Count__c == null ? 0 : manifest.Delivered_Count__c; manifest.Delivered_Count__c = current + deliveredByManifest.get(manifest.Id); }
update manifests; // No callout after the lock: a callout releases FOR UPDATE locks. // The locks are released when the transaction completes. }}Now the second crew’s save waits at the query until the first finishes, then reads 212 and writes 213. That’s the whole idea: lock, read fresh, change, finish. It’s bulkified the way Salesforce code has to be (one query for all the Manifests in the save, not one per Work Order), and it locks only the Manifests this save touches.
Notice what it does at 4 p.m., though. Every crew’s save on the same Manifest now lines up behind this lock. That’s the footprint on purpose, and it’s the right call for a count that must be correct. Module 2 shows how to make the line shorter, including moving this count out of the crew’s save altogether, and module 3 shows why the version you’d run in production recounts the Manifest instead of adding one.
And when the line is too long? The wait runs out at 10 seconds. The locking query throws a QueryException; an update that can’t get its lock throws a DmlException, and its status code says why:
try { ManifestDeliveredCount.add(deliveredByManifest);} catch (QueryException e) { // The FOR UPDATE query waited more than 10 seconds for the lock. // (QueryException covers other query errors too; log the message.) // Hand the work to a safe retry (module 3); don't swallow it.} catch (DmlException e) { if (e.getDmlType(0) == StatusCode.UNABLE_TO_LOCK_ROW) { // The update waited more than 10 seconds for a lock. // Hand the work to a safe retry (module 3); don't swallow it. } else { throw e; }}getDmlType “returns the value of the System.StatusCode enum” for a failed row (Exception Class and Built-In Exceptions (opens in a new tab), Apex Reference Guide, checked 2026-10-31), and Salesforce describes UNABLE_TO_LOCK_ROW as “a deadlock or timeout condition” (Core Data Types Used in API Calls (opens in a new tab), SOAP API Developer Guide, checked 2026-10-31). Catching it isn’t the fix. It’s how you see the collision and hand the work somewhere safe. The fix is in the design, and that’s the rest of this chapter.
One User Usually Hides the Problem
Section titled “One User Usually Hides the Problem”This is why concurrency problems are so hard to find during design. I create a record: it works. You create one: it works. The developer runs the unit tests: they pass. The business runs UAT: it works. Then production arrives with users, integrations, batch jobs, data loads, scheduled automation and agents, all writing at the same time, and something that worked perfectly one transaction at a time starts failing.
Nothing about the individual transaction changed. What changed was concurrency: the transactions started competing for the same records. Salesforce’s Well-Architected guidance says it plainly: “Concurrent execution reveals locking issues invisible in single-user testing.” When I teach parallel loads, I say it this way: it will work today and fail tomorrow, because whether it fails depends on what everything else in the org is doing at that moment.
Draw the Footprint of One Save
Section titled “Draw the Footprint of One Save”So stop asking only “what record am I saving?” and ask: what records does this save need to protect while it completes? That’s the lock footprint.
Take one crew’s tap at 4 p.m. The record they think they saved is one Work Order, now Delivered. Here’s what that save can reach:
- The Work Order itself, locked while the save runs.
- The Manifest it belongs to, locked because the Work Order points at it.
- The Retail Partner’s Account, locked for the same reason.
- Anything a roll-up or automation writes. If the Manifest counts its delivered Work Orders, the Manifest isn’t only locked; it’s saved, inside the crew’s save. If a flow updates the Service Appointment or posts a status for the partner, those records join the footprint too.
One record on the screen. Four or five records in the transaction. Now put a few hundred crews on top of that.
Relationships Change the Footprint
Section titled “Relationships Change the Footprint”This is where data modeling becomes concurrency architecture. We normally teach relationships in terms of meaning, cardinality, ownership, deletion and security. At scale I also want you asking: what does this relationship cause Salesforce to lock, and what does it cause Salesforce to do while the lock is held?
Start with the part people get wrong. “It’s only a lookup, so the parent isn’t involved.” It is. Salesforce’s engineering team put it plainly: “Every time a record is inserted or updated, Salesforce must lock the target records that are selected for each lookup field.” That’s how Salesforce keeps the parent from being deleted halfway through your save. A master-detail locks the master too. So a lookup doesn’t avoid the lock.
What a master-detail can add is work on the parent. A roll-up summary field lives on the master, and Salesforce’s own order of execution says what happens when a detail is saved: “If the record contains a roll-up summary field or is part of a cross-object workflow, performs calculations and updates the roll-up summary field in the parent record. Parent record goes through save procedure.” Then the same again for the grandparent. So with a roll-up, every Work Order save also runs the Manifest’s save: its validation rules, its triggers and flows, and any roll-up above it, all while the Manifest is locked.
That’s the difference I’d teach. Not “master-detail makes a bigger lock,” because Salesforce doesn’t publish its lock internals beyond what’s documented here. It’s that a roll-up makes more work happen while the lock is held, and a longer hold is a longer line. Salesforce’s Well-Architected framework names the same trade: master-detail gives you roll-up summaries “at the cost of tighter coupling,” while “lookups provide flexibility at the cost of custom rollup logic.” That’s why, when it was critical, I’ve moved a master-detail to a lookup and coded the roll-ups myself. The win comes when that counting runs outside the child’s save, later or in batches. Move it into a flow that updates the parent’s count in the same save, and you’ve put the problem straight back.
Test knowledge. Know that a lookup locks the record it points to when a child is inserted or updated, so lookups can cause lock contention too (lookup skew). Know that a roll-up summary makes the child’s save update the parent, and that the parent then goes through its own save (and the grandparent’s roll-up after it), inside the same transaction. Master-detail parents also carry chapter 7’s guidance on children per parent.
One Intersection
Section titled “One Intersection”When I teach deadlocks, I use two cars stuck at an intersection. Let me start with the intersection itself.
At 4 p.m., 330 trucks are each heading to a different house. Different routes, different stops, different Recipients. But near the DC, they all have to cross the same intersection. A truck clears it in a second, so on a normal day nobody notices. Only one can be in the middle at a time, though, and when they all arrive at once, the line backs up. The trucks at the back wait, and wait, and give up.
That intersection is the shared parent. Each crew’s save is a different Work Order, but every one of them has to cross the same Manifest, and only one at a time can hold its lock or run its roll-up. Salesforce’s waiting limit is the trucks at the back: after 10 seconds, the save fails with UNABLE_TO_LOCK_ROW. Chapter 7 called this skew, too many children under one parent, and Salesforce says what it does at 4 p.m.: when a parent piles up children, “row-lock contention emerges during concurrent operations.” This is the transaction-level reason skew hurts: a crowded parent isn’t only a modeling concentration, it’s a concurrency concentration.
Parallel Processing Exposes It
Section titled “Parallel Processing Exposes It”In my experience, the most common cause of lock errors is parallel processing at the same time, typically competing incoming APIs. System A sends updates. System B sends updates. A bulk job is running too. Each one looks reasonable on its own, but they arrive together and touch overlapping data. Then you see UNABLE_TO_LOCK_ROW, and the temptation is to call it an API problem or a random Salesforce error. I don’t. I want to know: what are these transactions all trying to lock? Find the overlap, and that’s usually where the architecture problem is hiding.
Data loads show it too. When I ask a class whether they run loads in serial or parallel mode, the usual answer is: go parallel, and if there are locking errors, switch to serial. Grab the speed, wait till you run into the speed bumps, then slow the car down. That’s fair, but you can do better than slowing down, and the next module shows how. Chapter 10 has the loading side: sort by parent, and keep all of one parent’s children in the same batch.
Two Trucks Stuck in the Intersection
Section titled “Two Trucks Stuck in the Intersection”Now the deadlock. A single lock that’s taken just makes the next transaction wait. A deadlock needs a multi-step lock: two transactions that each lock two records, in a different order. Transaction A updates the Retail Partner’s Account first, then needs the Manifest. Transaction B updated the Manifest first, and now needs the Account. It’s like two cars stuck at an intersection: each is waiting for the other to move, and neither one moves. That’s a deadlock. Something has to break the loop by failing a transaction: a database that detects the deadlock fails one of them, and which one is hard to predict; on Salesforce, a transaction that waits past 10 seconds fails too. So at least one side fails, sometimes both.
Salesforce works hard to keep its own engine out of that intersection. The Apex runtime “first locks sObject parent records, then children,” and “locks sObject records in order of ID when multiple records of the same type are being edited.” Your code has to do the same: the guide’s advice is “accessing tables and rows in the same order from all locations in an application.” Same order everywhere, and two trucks can’t each be waiting on the other.
Keep Transactions Short
Section titled “Keep Transactions Short”If I’m holding a lock, I want to hold it for as little time as I reasonably can. Back at the intersection: if every truck clears it quickly, traffic flows. If someone parks in the middle to make a phone call, everything backs up.
So when I look at a transaction, I ask: how many records does it touch? How much synchronous automation runs? Which related records get updated? What could happen later instead? How long is each lock held? That doesn’t mean “make everything asynchronous.” It means the synchronous transaction should contain only the work that truly has to happen before the user sees Saved.
Locking Protects Integrity. It Doesn’t Decide Business Truth.
Section titled “Locking Protects Integrity. It Doesn’t Decide Business Truth.”At 4 p.m. a coordinator changes a delivery window, the partner’s system sends a revised manifest line, and an agent decides the same delivery needs rescheduling. Locking stops those transactions from physically corrupting each other’s writes. It can’t tell you who should own that field, which update should win, or whether the agent should have been allowed to make the change. Those are architecture decisions, and we made them in chapters 6 and 9. Concurrency doesn’t replace those rules; it makes them more important. A lock can protect the transaction. It can’t define the truth.
Agents Add More Writers
Section titled “Agents Add More Writers”We used to design for humans, integrations and batch jobs. Now add agents. An agent can query, decide, call an action, update and retry, over and over, without getting tired. The platform’s locking rules haven’t changed. What changed is the number of actors and how they behave. Remember the Number of Zeros: a footprint that was fine with ten people may hurt with hundreds or thousands of automated actions.
So when somebody says, “We’re going to let the agent update deliveries,” my next questions are: which deliveries? Through what process? At what rate? What else writes them? And what does each save lock? Give agents a focused scope on updates, and have them call established, tested processes rather than writing records directly.
Beyond the exam: what AI changes. Agents add another concurrent writer to an architecture already serving users, integrations and automation. Give them narrow update scope and established, tested processes; don’t give an agent broad write access and hope the transaction model sorts it out.
Draw the Transaction Before You Tune It
Section titled “Draw the Transaction Before You Tune It”When I troubleshoot locking, I don’t start with “can we retry it?” Retry may be part of the answer eventually, but first I want to see the shape of the transaction. Draw it: this record changes; that parent is locked; that roll-up runs; that automation updates another record. If the same transaction also touches setup records such as users or permission sets, mark that too: DML on these setup objects “can’t be mixed with DML on non-setup sObjects in the same transaction,” so that’s a boundary you have to design around. Then overlay another transaction, and another. Where the footprints overlap is the hot spot, and once you can see the collision you can decide what to do about it. That’s module 2.
And when a retry does come, and something always retries, whether it’s the app, the integration layer or an agent, it has to be safe: multiple attempts mustn’t break something. That’s called idempotency. Press the elevator button five times and you still get one elevator. Bill a Retail Partner five times for one delivery, and you’ve billed them five times. Module 3 is about making the second attempt behave like the first.
What an Architect Decides Here
Section titled “What an Architect Decides Here”- Which records a business transaction may actually lock beyond the record the user or integration appears to be saving.
- Where relationships, roll-ups, lookups, sharing and automation create overlapping lock footprints.
- Which shared parent or referenced records can become concurrency hot spots as parallel volume increases.
- Which work truly belongs inside the synchronous transaction and which work can happen later.
- How human, integration, batch and agent writers overlap against the same data model.
One Save Is Rarely Just One Record
Section titled “One Save Is Rarely Just One Record”At 10 a.m. a crew taps Delivered and nothing happens to anyone else. At 4 p.m. the same tap, on a different Work Order, lands in a line of hundreds, all reaching for the same Manifest. The record didn’t change. The data model didn’t change. What changed was how many saves arrived at once. So before you tune anything, draw the footprint: this record, its parent, its roll-up, its automation. Then put the 4 p.m. traffic on top of it. Where the footprints overlap is where the architecture lives. Module 2 is about what to do there.
Next: Designing Out Lock Contention
Explore
Section titled “Explore”- On this site: HTTP Status Codes, every status code in plain words, including the 412 an optimistic-locking request gets back, with whose it is to fix and whether to try again.
- Salesforce, Apex Developer Guide (Winter ‘27): Locking Statements (opens in a new tab), Avoiding Deadlocks (opens in a new tab), sObjects That Can’t Be Used Together in DML Operations (opens in a new tab) and Triggers and Order of Execution (opens in a new tab), checked 2026-11-01.
- Salesforce, REST API Developer Guide: Conditional Request Headers (opens in a new tab) (
If-Unmodified-Since,If-Match), checked 2026-11-01. - Salesforce, Apex Reference Guide: Exception Class and Built-In Exceptions (opens in a new tab) (
getDmlType), and SOAP API Developer Guide: Core Data Types Used in API Calls (opens in a new tab) (UNABLE_TO_LOCK_ROW), checked 2026-11-01. - Salesforce engineering blog: Managing Lookup Skew to Avoid Record Lock Exceptions (opens in a new tab) (2013) and Salesforce Platform Record Locking and Concurrency (opens in a new tab) (the Record Locking Cheatsheet, 2014), checked 2026-11-01.
- Salesforce, Visualforce Developer Guide: Updating Records Issues (opens in a new tab) (collision detection), checked 2026-11-01.
- Salesforce Well-Architected: Reliability (opens in a new tab) and Resource and Cost Optimization (opens in a new tab), checked 2026-11-01.
- Beyond Salesforce: PostgreSQL 18 documentation, Explicit Locking (opens in a new tab) (deadlocks, consistent lock order, short transactions), and Microsoft Learn, Optimistic Concurrency (opens in a new tab) (both models and when each fits), checked 2026-11-01.
- Martin Fowler, Patterns of Enterprise Application Architecture, catalog: Optimistic Offline Lock (opens in a new tab) and Pessimistic Offline Lock (opens in a new tab), checked 2026-11-01.
