<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kamal Thakur</title>
    <description>The latest articles on DEV Community by Kamal Thakur (@kamal_thakur).</description>
    <link>https://dev.to/kamal_thakur</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3814201%2F91ac3b05-94cc-4883-b464-caf0d5a1a3e6.jpg</url>
      <title>DEV Community: Kamal Thakur</title>
      <link>https://dev.to/kamal_thakur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kamal_thakur"/>
    <language>en</language>
    <item>
      <title>Salesforce Record Locking and Concurrency – Salesforce Things You should Know</title>
      <dc:creator>Kamal Thakur</dc:creator>
      <pubDate>Tue, 07 Apr 2026 11:31:09 +0000</pubDate>
      <link>https://dev.to/kamal_thakur/salesforce-record-locking-and-concurrency-salesforce-things-you-should-know-50e9</link>
      <guid>https://dev.to/kamal_thakur/salesforce-record-locking-and-concurrency-salesforce-things-you-should-know-50e9</guid>
      <description>&lt;p&gt;If you build on Salesforce long enough, you will hit a lock. This post explains how Salesforce Record Locking and Concurrency work, why you see UNABLE_TO_LOCK_ROW, and how to write code that behaves correctly under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Salesforce locks records
&lt;/h2&gt;

&lt;p&gt;In a multi user system, two updates on the same record at the same time can corrupt data. To prevent that, Salesforce uses exclusive row locks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a transaction modifies a record, it takes a lock and holds it until the transaction ends.&lt;/li&gt;
&lt;li&gt;Another transaction that tries to update the same record waits for about 10 seconds. If the lock is not released in time, Salesforce throws UNABLE_TO_LOCK_ROW.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reads operations are different. A concurrent read operation sees the last committed version, not the in progress changes. That is normal isolation, meaning uncommitted changes are never visible to other transactions. &lt;/p&gt;

&lt;h2&gt;
  
  
  Reads vs writes in plain language
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Writes (DML) take a lock. Other writers wait.&lt;/li&gt;
&lt;li&gt;Reads (plain SOQL) do not lock and do not wait. They return the last committed data, which can be stale relative to an in flight update.&lt;/li&gt;
&lt;li&gt;Reads with FOR UPDATE ask for the same exclusive lock that DML uses. If a lock is held, the query waits. When it acquires the lock, it reads the freshest committed data and keeps the lock for the rest of the transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quick comparison&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxitckfxuzxfvmn97cbcs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxitckfxuzxfvmn97cbcs.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The classic race condition Salesforce Record Locking
&lt;/h2&gt;

&lt;p&gt;Start with an Opportunity where Amount = 100.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User A edits the Opportunity in the UI and saves Amount = 150. The write has started but is not committed yet. A holds the lock.&lt;/li&gt;
&lt;li&gt;User B triggers a process to add 30. B runs a plain SOQL query and reads Amount = 100 because A’s change is not committed.&lt;/li&gt;
&lt;li&gt;B sets 100 + 30 = 130 and tries to update. B waits for A’s lock.&lt;/li&gt;
&lt;li&gt;When A commits, the row is 150. B’s pending update still writes 130, which is wrong. The expected result was 180.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix: make B read using FOR UPDATE so B waits, then reads the fresh value, then writes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9w9az4umsmbymkw2ituf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9w9az4umsmbymkw2ituf.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The right way to read before you write
&lt;/h2&gt;

&lt;p&gt;Use FOR UPDATE when your logic must read a value and then write a value based on it, especially for money or counters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="kd"&gt;sharing&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OpportunityService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Safely increment Amount by `delta` using FOR UPDATE&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;incrementAmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="n"&gt;oppId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Simple retry for transient lock contention&lt;/span&gt;
        &lt;span class="n"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Opportunity&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;
                    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Opportunity&lt;/span&gt;
                    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;oppId&lt;/span&gt;
                    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
                &lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Amount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// holds the lock until commit&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// success&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DmlException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Lock timeout or deadlock. Retry a couple of times.&lt;/span&gt;
                &lt;span class="kt"&gt;Boolean&lt;/span&gt; &lt;span class="n"&gt;isLockIssue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;UNABLE_TO_LOCK_ROW'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isLockIssue&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="c1"&gt;// Optional: requeue work asynchronously instead of hot looping&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this does:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The FOR UPDATE query waits if the record is locked.&lt;/li&gt;
&lt;li&gt;When the query returns, you have the lock and a fresh value.&lt;/li&gt;
&lt;li&gt;Your subsequent DML keeps the lock until commit, preventing other writers from sneaking in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical guidance for Salesforce Record Locking
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Keep transactions short. Do not do heavy work between query and DML. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid long loops, large callouts, and complex triggers while holding locks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move DML to the end. Gather changes, then update once. This reduces lock time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use FOR UPDATE only when needed. Do not sprinkle it everywhere. Use it for read then write patterns where correctness matters, like financial amounts or inventory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Be mindful of data skew. Avoid relating more than 10,000 child records to a single parent in lookups. Skew increases the chance of contention.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handle lock errors. Catch UNABLE_TO_LOCK_ROW. Either retry a few times or requeue the work with Queueable so it runs later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Batch safely. In batch or flows that touch many related rows, group by parent to reduce cross record contention.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Design idempotent operations. If a retry runs twice, it should not corrupt totals.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debugging checklist for Salesforce Record Locking
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do you read a value and then write based on that value? If yes, consider FOR UPDATE.&lt;/li&gt;
&lt;li&gt;Are you doing DML in multiple places in a single transaction? Consolidate.&lt;/li&gt;
&lt;li&gt;Are you touching hot rows such as the same parent, the same owner, or the same counter object from many jobs? Stagger or shard the updates.&lt;/li&gt;
&lt;li&gt;Are long running triggers, workflows, or flows holding locks? Trim the work or move it async.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Salesforce Record Locking and Concurrency are there to protect your data. Plain SOQL reads last committed data and does not wait. DML and FOR UPDATE take locks and wait. When you must read then write correctly under contention, query with FOR UPDATE, keep the transaction short, and update once. Handle UNABLE_TO_LOCK_ROW with small retries or by deferring work.&lt;/p&gt;

&lt;p&gt;That is the stable pattern that keeps totals correct and errors rare.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>salesforce</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
