<?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: Samcorp</title>
    <description>The latest articles on DEV Community by Samcorp (@samcorp_388df23e8f0e61ab6).</description>
    <link>https://dev.to/samcorp_388df23e8f0e61ab6</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2447383%2Ff75605b3-e11a-48ef-ab55-78bb340aa7b0.png</url>
      <title>DEV Community: Samcorp</title>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samcorp_388df23e8f0e61ab6"/>
    <language>en</language>
    <item>
      <title>Our Accessibility Audit Found 340 Issues Here's the Pattern</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:44:02 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/our-accessibility-audit-found-340-issues-heres-the-pattern-49kg</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/our-accessibility-audit-found-340-issues-heres-the-pattern-49kg</guid>
      <description>&lt;p&gt;We expected the accessibility audit to produce a long list of unrelated problems.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmyvdepmje9a23ijl7t6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmyvdepmje9a23ijl7t6.png" alt="Our Accessibility Audit Found 340 Issues Here's the Pattern" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It found &lt;strong&gt;340 issues&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But the interesting part was not the number.&lt;/p&gt;

&lt;p&gt;It was the pattern.&lt;/p&gt;

&lt;p&gt;A small group of recurring mistakes accounted for most of the failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;340 findings
     ↓
A handful of repeated patterns
     ↓
A much smaller number of root causes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changed how we approached the fixes.&lt;/p&gt;

&lt;p&gt;Instead of repairing 340 problems individually, we started fixing the components and design decisions creating them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The counts in this post are representative examples based on a typical mixed automated/manual audit. Replace them with your actual audit results before presenting them as project data.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What the Audit Found
&lt;/h2&gt;

&lt;p&gt;We grouped the 340 findings by root cause.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue Type&lt;/th&gt;
&lt;th&gt;Findings&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Color contrast&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;td&gt;32.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form labels&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;18.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image alternative text&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyboard and focus&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;11.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links and button names&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;9.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heading and page structure&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;7.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other issues&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;340&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distribution surprised us.&lt;/p&gt;

&lt;p&gt;Nearly two-thirds of the findings came from just three areas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contrast
+
Forms
+
Images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was not unusual.&lt;/p&gt;

&lt;p&gt;The 2026 WebAIM Million study found low-contrast text on &lt;strong&gt;83.9%&lt;/strong&gt; of analyzed home pages, missing image alternative text on &lt;strong&gt;53.1%&lt;/strong&gt;, and missing form labels on &lt;strong&gt;51%&lt;/strong&gt;. WebAIM also found that six common error types represented 96% of all automatically detected errors.&lt;/p&gt;

&lt;p&gt;Our audit reinforced the same lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Accessibility problems often repeat because the underlying implementation repeats.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Pattern #1: Contrast Problems Were Mostly Design-System Problems
&lt;/h2&gt;

&lt;p&gt;The largest category contained 112 findings.&lt;/p&gt;

&lt;p&gt;At first they looked like individual color mistakes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card-description&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#a6a6a6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&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;p&gt;But the same colors appeared across cards, tables, helper text, placeholders, secondary buttons, and empty states.&lt;/p&gt;

&lt;p&gt;Fixing every page individually would have been the wrong approach.&lt;/p&gt;

&lt;p&gt;The real problem lived in the design tokens.&lt;/p&gt;

&lt;p&gt;Instead of changing dozens of components, we changed the shared semantic color definitions and then retested the components that depended on them.&lt;/p&gt;

&lt;p&gt;Fixing shared components and design tokens is also part of building &lt;a href="https://sdlccorp.com/web-development-company/" rel="noopener noreferrer"&gt;scalable web platforms&lt;/a&gt;, because one accessible component can improve dozens of pages at once.&lt;/p&gt;

&lt;p&gt;The lesson was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the same accessibility issue appears everywhere, look for the shared system creating it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;WCAG 2.2 includes minimum contrast requirements for text as well as requirements for visual information used to identify interface components and states.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern #2: Forms Looked Labeled but Were Not Programmatically Labeled
&lt;/h2&gt;

&lt;p&gt;The next 63 findings involved forms.&lt;/p&gt;

&lt;p&gt;Visually, many fields looked perfectly understandable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email Address
[________________]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But some implementations were closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Email Address&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sighted user could understand the relationship.&lt;/p&gt;

&lt;p&gt;Assistive technology could not necessarily determine it.&lt;/p&gt;

&lt;p&gt;We replaced patterns like that with explicit labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email Address&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Icon-only controls needed accessible names too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Clear search"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This category taught us that accessibility cannot be judged from screenshots alone.&lt;/p&gt;

&lt;p&gt;The visual interface and the accessibility tree are different representations of the same product.&lt;/p&gt;

&lt;p&gt;WCAG's Name, Role, Value requirement exists so user interface components can expose information that assistive technologies can understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern #3: &lt;code&gt;alt&lt;/code&gt; Text Was Not Just Missing — It Was Often Wrong
&lt;/h2&gt;

&lt;p&gt;We found 49 image-related issues.&lt;/p&gt;

&lt;p&gt;Some images had no &lt;code&gt;alt&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;Others technically had one but used values like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"chart.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"team-photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"team-photo.jpg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those technically contain text.&lt;/p&gt;

&lt;p&gt;They do not necessarily communicate anything useful.&lt;/p&gt;

&lt;p&gt;We started classifying images by purpose.&lt;/p&gt;

&lt;p&gt;Informative images received meaningful descriptions.&lt;/p&gt;

&lt;p&gt;Decorative images used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;alt=""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functional images needed text that described the action or destination rather than simply describing how the image looked.&lt;/p&gt;

&lt;p&gt;This distinction mattered more than simply asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does every image have an &lt;code&gt;alt&lt;/code&gt; attribute?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better question was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does the text alternative provide the information this image contributes?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;WebAIM's 2026 analysis found that &lt;strong&gt;16.2% of images&lt;/strong&gt; on the million home pages lacked alternative text, and it also identified large numbers of questionable or repetitive alternative text.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern #4: Mouse Testing Hid Keyboard Problems
&lt;/h2&gt;

&lt;p&gt;Thirty-eight findings appeared only when we stopped using the mouse.&lt;/p&gt;

&lt;p&gt;We pressed &lt;code&gt;Tab&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Suddenly we found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dropdowns that could not be opened

Dialogs that trapped focus

Clickable divs that keyboard users could not activate

Focus indicators that disappeared

Focus moving somewhere unexpected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One common anti-pattern looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"openMenu()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Menu
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The better solution was often simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"openMenu()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Menu
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native HTML already provides keyboard behavior, semantics, and focus support.&lt;/p&gt;

&lt;p&gt;We had occasionally rebuilt those features ourselves and produced worse results.&lt;/p&gt;

&lt;p&gt;WCAG 2.2 requires functionality to be operable through a keyboard, and its Focus Visible criterion requires users to be able to see where keyboard focus currently is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern #5: Icon Buttons Were Easy to See but Hard to Understand
&lt;/h2&gt;

&lt;p&gt;Thirty-one findings involved empty links, empty buttons, or controls without useful accessible names.&lt;/p&gt;

&lt;p&gt;Our interfaces contained many controls like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;svg&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sighted user saw a trash icon.&lt;/p&gt;

&lt;p&gt;A screen reader could encounter an unnamed button.&lt;/p&gt;

&lt;p&gt;The fix might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Delete project"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the larger lesson was not “add ARIA everywhere.”&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every interactive control needs a meaningful name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Where native visible text could do the job, we preferred that.&lt;/p&gt;

&lt;p&gt;ARIA was useful when the visual design genuinely required an icon-only control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern #6: Heading Problems Came From Styling Decisions
&lt;/h2&gt;

&lt;p&gt;We found 27 structural issues.&lt;/p&gt;

&lt;p&gt;Some pages jumped from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;H1
 ↓
H3
 ↓
H5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because developers chose heading elements based on their default visual size.&lt;/p&gt;

&lt;p&gt;That is backwards.&lt;/p&gt;

&lt;p&gt;We changed the approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"small-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Account Settings
&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of choosing &lt;code&gt;&amp;lt;h4&amp;gt;&lt;/code&gt; simply because it visually looked smaller.&lt;/p&gt;

&lt;p&gt;CSS controls appearance.&lt;/p&gt;

&lt;p&gt;HTML should describe structure.&lt;/p&gt;

&lt;p&gt;This matters because headings are an important navigation mechanism for screen-reader users. The 2026 WebAIM study found skipped heading levels on &lt;strong&gt;41.8% of analyzed home pages&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Important Finding Was Duplication
&lt;/h2&gt;

&lt;p&gt;Once we grouped the 340 issues, something became obvious.&lt;/p&gt;

&lt;p&gt;Many findings were not 340 separate bugs.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;21 contrast issues
→ one incorrect text token

17 missing labels
→ one shared form component

12 unnamed buttons
→ one icon-button component

9 focus problems
→ one modal implementation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly the remediation strategy changed.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find issue
   ↓
Fix page
   ↓
Find next issue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we moved toward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find pattern
   ↓
Find shared component
   ↓
Fix root cause
   ↓
Regression test
   ↓
Remove many findings at once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the real value of the &lt;strong&gt;accessibility audit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It showed us where accessibility debt was being generated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automated Testing Was Useful — but It Was Not the Audit
&lt;/h2&gt;

&lt;p&gt;Automated tools found many obvious problems quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contrast

Missing labels

Missing alt attributes

Empty links

Empty buttons

Some ARIA problems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But automation could not answer every important question.&lt;/p&gt;

&lt;p&gt;It could not reliably tell us whether:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The keyboard flow made sense

Alternative text was actually useful

A modal announced itself correctly

Focus moved to the right place

Error messages were understandable

A workflow was usable with a screen reader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why our audit combined automated checks with keyboard testing and manual review.&lt;/p&gt;

&lt;p&gt;WebAIM makes the same limitation explicit: automated tools detect only a subset of accessibility failures, and having zero automatically detected errors does &lt;strong&gt;not&lt;/strong&gt; prove that a page conforms to WCAG.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Changed After the Audit
&lt;/h2&gt;

&lt;p&gt;The biggest improvement was not fixing those 340 findings.&lt;/p&gt;

&lt;p&gt;It was changing how new findings were prevented.&lt;/p&gt;

&lt;p&gt;Accessibility checks moved earlier into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Design
   ↓
Component development
   ↓
Code review
   ↓
Automated testing
   ↓
Manual keyboard testing
   ↓
Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shared components became particularly important.&lt;/p&gt;

&lt;p&gt;If we could make one button, form field, dialog, or navigation component accessible by default, dozens of future screens benefited automatically.&lt;/p&gt;

&lt;p&gt;Accessibility stopped being something we checked at the end.&lt;/p&gt;

&lt;p&gt;We started treating &lt;a href="https://sdlccorp.com/ui-ux-design-company/" rel="noopener noreferrer"&gt;accessible UI/UX design&lt;/a&gt; as part of the product system itself, so contrast, component behavior, hierarchy, and WCAG requirements were considered before development reached QA.&lt;/p&gt;

&lt;p&gt;It became part of how the component system was built.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Data Changed Our Priorities
&lt;/h2&gt;

&lt;p&gt;Before the audit, accessibility felt like a huge collection of rules.&lt;/p&gt;

&lt;p&gt;After grouping the findings, the work became more concrete.&lt;/p&gt;

&lt;p&gt;We did not need to solve every possible accessibility problem simultaneously.&lt;/p&gt;

&lt;p&gt;We needed to stop repeatedly shipping the same problems.&lt;/p&gt;

&lt;p&gt;The data showed where to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fix contrast tokens.

Fix form components.

Fix image handling.

Fix keyboard interaction.

Fix accessible names.

Fix document structure.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was much more actionable than saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make the website accessible.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The most useful number from the audit was not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;340 issues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A few patterns created most of them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changes accessibility work from an endless cleanup exercise into an engineering problem.&lt;/p&gt;

&lt;p&gt;If one component creates 25 failures, fix the component.&lt;/p&gt;

&lt;p&gt;If one design token creates 40 contrast problems, fix the token.&lt;/p&gt;

&lt;p&gt;If one modal pattern breaks keyboard navigation across the application, fix the pattern.&lt;/p&gt;

&lt;p&gt;Accessibility scales when the &lt;strong&gt;correct behavior becomes the default behavior&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;accessibility audit&lt;/strong&gt; should not finish with a spreadsheet containing hundreds of rows.&lt;/p&gt;

&lt;p&gt;That spreadsheet is the beginning.&lt;/p&gt;

&lt;p&gt;Group the findings.&lt;/p&gt;

&lt;p&gt;Look for repetition.&lt;/p&gt;

&lt;p&gt;Trace repeated failures back to shared components, design tokens, and engineering conventions.&lt;/p&gt;

&lt;p&gt;Then fix the systems creating the issues.&lt;/p&gt;

&lt;p&gt;Our 340 findings looked overwhelming at first.&lt;/p&gt;

&lt;p&gt;Once we analyzed the data, the problem became much simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;We did not have 340 independent accessibility problems. We had a small number of accessibility patterns repeated across the product.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And repeated problems are exactly the kind of thing engineering systems are good at fixing.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>What Broke When We Adopted React Server Components</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:30:29 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/what-broke-when-we-adopted-react-server-components-3p7h</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/what-broke-when-we-adopted-react-server-components-3p7h</guid>
      <description>&lt;p&gt;We adopted React Server Components expecting a fairly simple win:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs95v3rsh517mld0bs34v.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs95v3rsh517mld0bs34v.png" alt="What Broke When We Adopted React Server Components" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Less client JavaScript
        ↓
More server-side data fetching
        ↓
Faster pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some of that happened.&lt;/p&gt;

&lt;p&gt;But the migration also broke assumptions that had quietly existed across our frontend for years.&lt;/p&gt;

&lt;p&gt;Components that used to be interchangeable suddenly belonged to different environments. Shared objects could no longer always cross component boundaries. Data fetching moved closer to the database but sometimes became slower because we created waterfalls.&lt;/p&gt;

&lt;p&gt;The biggest lesson from our &lt;strong&gt;React Server Components issues&lt;/strong&gt; was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RSC was not just a rendering optimization. It changed the architecture of the application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a representative engineering post-mortem. The examples describe common RSC migration problems rather than a specific customer deployment.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What We Expected
&lt;/h2&gt;

&lt;p&gt;Our previous mental model was straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server
  ↓
HTML + JavaScript
  ↓
React in the browser
  ↓
API calls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Server Components, the architecture became closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server Components
       ↓
RSC Payload
       ↓
Client Components
       ↓
Browser interaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefit was obvious.&lt;/p&gt;

&lt;p&gt;Code that only needed to run on the server no longer needed to become browser JavaScript.&lt;/p&gt;

&lt;p&gt;But we underestimated how important the boundary between those two environments would become.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #1: &lt;code&gt;"use client"&lt;/code&gt; Spread Further Than Expected
&lt;/h2&gt;

&lt;p&gt;Our first problem looked harmless.&lt;/p&gt;

&lt;p&gt;A component needed state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Filters
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;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;p&gt;Server Components cannot maintain client-side state or register browser event handlers, so the component needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That part was expected.&lt;/p&gt;

&lt;p&gt;What surprised us was what happened to its dependencies.&lt;/p&gt;

&lt;p&gt;Once a module becomes part of the client component graph, code imported beneath that boundary can also become client-side code.&lt;/p&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page
 ↓
Dashboard
 ↓
Filters
 ↓
Utilities
 ↓
Formatting library
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could accidentally turn more code into browser JavaScript than we intended.&lt;/p&gt;

&lt;p&gt;React's documentation explicitly describes &lt;code&gt;"use client"&lt;/code&gt; as a boundary in the &lt;strong&gt;module dependency tree&lt;/strong&gt;, not merely a marker for one component.&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://sdlccorp.com/web-development-company/" rel="noopener noreferrer"&gt;scalable web development&lt;/a&gt; becomes less about component count and more about defining clean boundaries between server-rendered logic and client-side interactivity.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Changed
&lt;/h3&gt;

&lt;p&gt;We stopped placing &lt;code&gt;"use client"&lt;/code&gt; high in the tree.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dashboard
  "use client"
      ↓
Everything below it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we moved the boundary downward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dashboard        Server
├── Header       Server
├── Report       Server
└── Filters      Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep the interactive island as small as practical.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break #2: Browser Assumptions Started Failing
&lt;/h2&gt;

&lt;p&gt;Years of React development had left browser assumptions everywhere.&lt;/p&gt;

&lt;p&gt;Components accessed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;window
document
localStorage
navigator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some third-party libraries also expected a browser environment immediately during module evaluation.&lt;/p&gt;

&lt;p&gt;Then those components moved into server-rendered code.&lt;/p&gt;

&lt;p&gt;Suddenly we saw errors around unavailable browser APIs.&lt;/p&gt;

&lt;p&gt;The solution was not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add "use client" everywhere.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would have removed much of the reason for adopting Server Components.&lt;/p&gt;

&lt;p&gt;Instead, we separated concerns.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProductPage
   │
   ├── ProductDetails      Server
   ├── Recommendations    Server
   └── RecentlyViewed     Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the feature that genuinely needed &lt;code&gt;localStorage&lt;/code&gt; remained client-side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;A component needing browser APIs is not a failure.&lt;/p&gt;

&lt;p&gt;But it needs an explicit client boundary.&lt;/p&gt;

&lt;p&gt;React's current documentation makes the same distinction: Server Components cannot use interactive browser behavior or most stateful Hooks, while Client Components handle that work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #3: Our Shared Objects Stopped Crossing the Boundary
&lt;/h2&gt;

&lt;p&gt;Before RSC, we passed rich JavaScript objects around freely.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;p&gt;Then we tried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductPrice&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;across a server-to-client boundary.&lt;/p&gt;

&lt;p&gt;That exposed another rule.&lt;/p&gt;

&lt;p&gt;Values passed from Server Components into Client Components need to be serializable.&lt;/p&gt;

&lt;p&gt;Plain data works well.&lt;/p&gt;

&lt;p&gt;Custom class instances do not.&lt;/p&gt;

&lt;p&gt;React's documented serializable types include primitives, arrays, maps, sets, dates, plain objects, and several other supported values—but ordinary class instances are not supported across that boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Changed
&lt;/h3&gt;

&lt;p&gt;Instead of passing domain objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we passed DTO-style data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the client handled presentation.&lt;/p&gt;

&lt;p&gt;This pushed us toward a cleaner rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server domain model
      ↓
Serializable view model
      ↓
Client component
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was annoying at first.&lt;/p&gt;

&lt;p&gt;Architecturally, it ended up being healthier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #4: Moving Data Fetching to the Server Created Waterfalls
&lt;/h2&gt;

&lt;p&gt;One reason we adopted Server Components was cleaner data fetching.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&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;p&gt;we could write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderList&lt;/span&gt; &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much nicer.&lt;/p&gt;

&lt;p&gt;Then we accidentally wrote pages like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recommendations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRecommendations&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those requests were independent, we had created a waterfall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getUser()
   ↓
wait
   ↓
getOrders()
   ↓
wait
   ↓
getRecommendations()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code looked clean.&lt;/p&gt;

&lt;p&gt;The request was slow.&lt;/p&gt;

&lt;p&gt;Next.js documentation explicitly warns about accidental sequential data fetching and recommends parallelizing independent work when possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;Where requests were independent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recommendations&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;getRecommendations&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;p&gt;And where parts of the page could arrive later, we used Suspense and streaming rather than blocking the whole route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page shell
   ↓
Important content
   ↓
──────── streamed later ────────
   ↓
Recommendations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Streaming lets slower parts of a route arrive separately instead of blocking everything above them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;Server-side data fetching does not automatically mean fast data fetching.&lt;/p&gt;

&lt;p&gt;The dependency graph still matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #5: Caching Became Part of Application Correctness
&lt;/h2&gt;

&lt;p&gt;This issue was framework-specific rather than an RSC rule itself.&lt;/p&gt;

&lt;p&gt;Once more rendering happened on the server, our framework's caching and revalidation behavior suddenly mattered much more.&lt;/p&gt;

&lt;p&gt;Previously, developers often thought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Call API
   ↓
Receive current data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we needed to ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Was this rendered dynamically?

Was the result cached?

When does it expire?

What invalidates it?

Should this page be static at all?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bugs were frustrating because stale data often looked valid.&lt;/p&gt;

&lt;p&gt;A product page displaying yesterday's inventory does not throw an exception.&lt;/p&gt;

&lt;p&gt;It simply lies quietly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Changed
&lt;/h3&gt;

&lt;p&gt;Every important data source received an explicit caching decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product description
→ cache aggressively

Current inventory
→ fresher policy

User session
→ request-specific

Pricing
→ explicit business TTL / invalidation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We stopped treating caching as a performance detail.&lt;/p&gt;

&lt;p&gt;It became part of data correctness.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #6: Server Functions Looked Safer Than They Actually Were
&lt;/h2&gt;

&lt;p&gt;Server Functions made mutations elegant.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// update order&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a Client Component could invoke that function without manually building another traditional API route.&lt;/p&gt;

&lt;p&gt;The abstraction felt safe because the function lived on the server.&lt;/p&gt;

&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;React's documentation explicitly warns that arguments sent to Server Functions are &lt;strong&gt;fully client-controlled&lt;/strong&gt; and that authorization must be performed inside the server-side operation.&lt;/p&gt;

&lt;p&gt;This is wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoiceId&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;p&gt;A better pattern is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requireUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoiceId&lt;/span&gt; &lt;span class="p"&gt;}&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="nf"&gt;canDeleteInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invoice&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&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;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoiceId&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;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;"use server"&lt;/code&gt; tells React where code executes.&lt;/p&gt;

&lt;p&gt;It does not mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trusted
authorized
validated
safe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are still our responsibilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #7: Debugging Moved Away From the Browser
&lt;/h2&gt;

&lt;p&gt;Before the migration, developers were used to finding most problems in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser DevTools
Console
Network tab
Client stack trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After RSC, an error might occur during:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server rendering
database access
RSC serialization
Server Function execution
stream generation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser sometimes only showed the result of that failure.&lt;/p&gt;

&lt;p&gt;The useful stack trace lived on the server.&lt;/p&gt;

&lt;p&gt;That changed our observability requirements.&lt;/p&gt;

&lt;p&gt;We added clearer logging around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Route
Request ID
User/session context
Server component
Data fetch
Server Function
Duration
Failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our mental model changed from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a frontend application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a distributed full-stack application that happens to use React.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break #8: Our Tests Were Testing the Wrong Architecture
&lt;/h2&gt;

&lt;p&gt;We had many tests built around client-rendered React.&lt;/p&gt;

&lt;p&gt;They assumed components could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use browser APIs&lt;/li&gt;
&lt;li&gt;Call mocked REST endpoints&lt;/li&gt;
&lt;li&gt;Run entirely inside a DOM test environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Server Components changed those assumptions.&lt;/p&gt;

&lt;p&gt;A component like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AccountPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findFirst&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Account&lt;/span&gt; &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is no longer just a UI function.&lt;/p&gt;

&lt;p&gt;It touches a server-side dependency.&lt;/p&gt;

&lt;p&gt;So we separated testing into layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server Logic
&lt;/h3&gt;

&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queries
authorization
transformations
Server Functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Client Components
&lt;/h3&gt;

&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interaction
state
browser behavior
accessibility
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration
&lt;/h3&gt;

&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server Component
      ↓
Client boundary
      ↓
mutation
      ↓
updated UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trying to force all three into the old frontend test strategy created unnecessary pain.&lt;/p&gt;

&lt;p&gt;We also started treating the migration as a broader &lt;a href="https://sdlccorp.com/services/testing-services/" rel="noopener noreferrer"&gt;software testing and QA&lt;/a&gt; problem, with separate coverage for server logic, client interaction, integration behavior, performance, and regression risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  One More Lesson: RSC Added a New Security Patch Surface
&lt;/h2&gt;

&lt;p&gt;There was another operational lesson we could not ignore.&lt;/p&gt;

&lt;p&gt;React disclosed a critical React Server Components vulnerability in December 2025, followed by additional denial-of-service and source-exposure issues. React recommended immediate upgrades to patched packages.&lt;/p&gt;

&lt;p&gt;That changed our dependency policy.&lt;/p&gt;

&lt;p&gt;RSC infrastructure was no longer something we could treat like passive frontend tooling.&lt;/p&gt;

&lt;p&gt;It was server infrastructure exposed to requests.&lt;/p&gt;

&lt;p&gt;We added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React/RSC security advisories
       ↓
Dependency review
       ↓
Framework compatibility check
       ↓
Rapid patch rollout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broader lesson was not that Server Components are inherently unsafe.&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once React becomes part of your server execution path, React security updates become production server updates.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Actually Worked
&lt;/h2&gt;

&lt;p&gt;After the rough migration, several rules made the architecture much easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Server by Default
&lt;/h3&gt;

&lt;p&gt;Components stayed server-side unless they genuinely required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state
effects
events
browser APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Small Client Islands
&lt;/h3&gt;

&lt;p&gt;We pushed &lt;code&gt;"use client"&lt;/code&gt; as far down the component tree as practical.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Serializable Boundaries
&lt;/h3&gt;

&lt;p&gt;Server Components returned simple, intentional view models to Client Components.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Parallel Data Fetching
&lt;/h3&gt;

&lt;p&gt;Independent server requests started together.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Suspense Around Slow Work
&lt;/h3&gt;

&lt;p&gt;Slow data stopped blocking unrelated UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Explicit Cache Decisions
&lt;/h3&gt;

&lt;p&gt;Every important data source had a freshness policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Authorization Inside Server Functions
&lt;/h3&gt;

&lt;p&gt;Never at the button.&lt;/p&gt;

&lt;p&gt;Never only in the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Server Observability
&lt;/h3&gt;

&lt;p&gt;Logs and tracing became part of frontend debugging.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture We Ended Up With
&lt;/h2&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React Client
     ↓
API
     ↓
Backend
     ↓
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Server
                   │
        ┌──────────┴──────────┐
        ▼                     ▼
 Server Components       Data Layer
        │                     │
        └──────────┬──────────┘
                   ▼
              RSC Payload
                   │
                   ▼
            Client Islands
                   │
                   ▼
            User Interaction
                   │
                   ▼
           Server Functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither architecture is automatically better.&lt;/p&gt;

&lt;p&gt;But the second requires much more intentional boundary design.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Would Do Differently
&lt;/h2&gt;

&lt;p&gt;If we adopted RSC again, we would start with boundaries rather than components.&lt;/p&gt;

&lt;h3&gt;
  
  
  First: Map Interactive Areas
&lt;/h3&gt;

&lt;p&gt;Identify everything requiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useState
useEffect
events
browser APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Second: Map Server-Only Data
&lt;/h3&gt;

&lt;p&gt;Identify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database access
secrets
internal APIs
authorization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Third: Define the Boundary
&lt;/h3&gt;

&lt;p&gt;Decide what data is allowed to cross from server to client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fourth: Inspect Data Dependencies
&lt;/h3&gt;

&lt;p&gt;Find sequential fetches before they become waterfalls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fifth: Define Cache Rules
&lt;/h3&gt;

&lt;p&gt;Do not wait for stale-data bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sixth: Rebuild the Test Strategy
&lt;/h3&gt;

&lt;p&gt;Treat server logic and browser interaction as different layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seventh: Add Server Observability
&lt;/h3&gt;

&lt;p&gt;Before production traffic arrives.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The biggest mistake was treating React Server Components as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React
+
better server rendering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The better model was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React
+
two execution environments
+
a network boundary
+
serialization
+
server data access
+
client interactivity
+
new security responsibilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That explains most of the &lt;strong&gt;React Server Components issues&lt;/strong&gt; we encountered.&lt;/p&gt;

&lt;p&gt;The code still looked like React.&lt;/p&gt;

&lt;p&gt;The architecture was no longer purely frontend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;React Server Components can be valuable.&lt;/p&gt;

&lt;p&gt;They can reduce client-side JavaScript, keep server-only logic off the browser, and make server data access feel natural.&lt;/p&gt;

&lt;p&gt;But adopting them successfully requires asking a different question.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which components should we convert to Server Components?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where should this code execute?

Which code really needs the browser?

What data is allowed to cross the boundary?

Which requests can run in parallel?

What must stay fresh?

Where is authorization enforced?

Where will failures appear?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we started answering those questions explicitly, the migration became much easier.&lt;/p&gt;

&lt;p&gt;The biggest lesson was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;React Server Components are not just a component feature. They are an application-boundary decision.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
    <item>
      <title>Five AI Features We Built and Then Deleted</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Thu, 03 Sep 2026 06:21:08 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/five-ai-features-we-built-and-then-deleted-4na</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/five-ai-features-we-built-and-then-deleted-4na</guid>
      <description>&lt;p&gt;We shipped five AI features that worked, Then we deleted them.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnk7ya337iw9fis11zwb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnk7ya337iw9fis11zwb.png" alt="Five AI Features We Built and Then Deleted" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not because the models crashed. Not because the APIs stopped responding. And not because AI suddenly became useless.&lt;/p&gt;

&lt;p&gt;The features simply failed to create enough value to justify the complexity they introduced.&lt;/p&gt;

&lt;p&gt;That distinction changed how we think about &lt;strong&gt;AI feature failure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A feature can be technically impressive and still be the wrong product decision.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The examples below are representative engineering scenarios intended to explain common AI product failures. They are not claims about a specific customer or production deployment.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Features Looked Good in Demos
&lt;/h2&gt;

&lt;p&gt;This was the dangerous part.&lt;/p&gt;

&lt;p&gt;Every feature had a convincing demo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt
  ↓
Model
  ↓
Useful-looking output
  ↓
Everyone says, "That's cool."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problems appeared later.&lt;/p&gt;

&lt;p&gt;Real users brought incomplete data, ambiguous requests, unusual workflows, latency expectations, and very little patience for confidently wrong answers.&lt;/p&gt;

&lt;p&gt;By the end, five features had taught us five different lessons.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Why We Removed It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI Search Answers&lt;/td&gt;
&lt;td&gt;Users trusted incorrect summaries too easily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Code Review Bot&lt;/td&gt;
&lt;td&gt;Too much low-value feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic Support Replies&lt;/td&gt;
&lt;td&gt;Confidence was hard to measure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Dashboard Insights&lt;/td&gt;
&lt;td&gt;It described correlation like causation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Universal AI Assistant&lt;/td&gt;
&lt;td&gt;Cost and complexity grew faster than usage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. AI Search Answers
&lt;/h2&gt;

&lt;p&gt;The idea was straightforward.&lt;/p&gt;

&lt;p&gt;Instead of making users search documentation manually, we would retrieve relevant documents and generate a direct answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Question
   ↓
Vector Search
   ↓
Relevant Documents
   ↓
LLM
   ↓
Answer + Sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prototype worked surprisingly well.&lt;/p&gt;

&lt;p&gt;Then we tested messy queries.&lt;/p&gt;

&lt;p&gt;Sometimes retrieval found the correct document, but the model summarized it incorrectly.&lt;/p&gt;

&lt;p&gt;Sometimes the answer was correct, but the citation pointed to a document that only partially supported it.&lt;/p&gt;

&lt;p&gt;Sometimes two documents contained conflicting information, and the model quietly chose one.&lt;/p&gt;

&lt;p&gt;The dangerous failures were not obviously ridiculous.&lt;/p&gt;

&lt;p&gt;They looked reasonable.&lt;/p&gt;

&lt;p&gt;That made them harder to detect.&lt;/p&gt;

&lt;p&gt;We added retrieval scores, citation validation, better prompts, and fallback behavior. Accuracy improved, but the product problem remained: users often treated a fluent answer as authoritative.&lt;/p&gt;

&lt;p&gt;NIST specifically highlights the risk of people over-relying on AI output, sometimes referred to as automation bias.&lt;/p&gt;

&lt;p&gt;We eventually removed the generated answer from the primary search experience.&lt;/p&gt;

&lt;p&gt;Search stayed.&lt;/p&gt;

&lt;p&gt;Retrieval stayed.&lt;/p&gt;

&lt;p&gt;The AI-written conclusion did not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;Sometimes the better AI feature is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find the right information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Interpret the information for the user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The AI Code Review Bot
&lt;/h2&gt;

&lt;p&gt;This feature was popular internally at first.&lt;/p&gt;

&lt;p&gt;Every pull request received automated review comments.&lt;/p&gt;

&lt;p&gt;The bot looked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Potential bugs
Security concerns
Missing edge cases
Naming problems
Complex functions
Test gaps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically, it worked.&lt;/p&gt;

&lt;p&gt;Operationally, it became annoying.&lt;/p&gt;

&lt;p&gt;One pull request might receive eight comments.&lt;/p&gt;

&lt;p&gt;Perhaps one mattered.&lt;/p&gt;

&lt;p&gt;Developers quickly learned to skim the entire review because most comments were suggestions rather than actual defects.&lt;/p&gt;

&lt;p&gt;That created a strange failure mode.&lt;/p&gt;

&lt;p&gt;The bot was producing &lt;strong&gt;more review activity while reducing attention&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After enough low-value comments, even the useful warning looked like noise.&lt;/p&gt;

&lt;p&gt;We tried raising confidence thresholds.&lt;/p&gt;

&lt;p&gt;Then we restricted the bot to security and correctness issues.&lt;/p&gt;

&lt;p&gt;That helped, but another problem appeared: deterministic checks were often better handled by existing tools.&lt;/p&gt;

&lt;p&gt;A linter should catch formatting.&lt;/p&gt;

&lt;p&gt;Static analysis should catch known patterns.&lt;/p&gt;

&lt;p&gt;Tests should verify known behavior.&lt;/p&gt;

&lt;p&gt;We also started treating model behavior like &lt;a href="https://sdlccorp.com/services/testing-services/" rel="noopener noreferrer"&gt;software testing and QA&lt;/a&gt;, with clear test cases, edge conditions, regression checks, and measurable failure criteria.&lt;br&gt;
This is a natural fit because the SDLC Corp testing page covers functional, automation, performance, security, API, and AI-powered QA&lt;/p&gt;

&lt;p&gt;Using an LLM for problems that deterministic tools already solved added cost and uncertainty.&lt;/p&gt;

&lt;p&gt;So we deleted the general-purpose review bot.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;AI should not replace reliable deterministic automation simply because AI can produce an answer.&lt;/p&gt;

&lt;p&gt;Use models where judgment is genuinely required.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Automatic Customer Support Replies
&lt;/h2&gt;

&lt;p&gt;This was the feature everyone expected to save the most time.&lt;/p&gt;

&lt;p&gt;Incoming ticket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"My invoice shows two charges."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"I can help with that. It appears..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first version generated drafts.&lt;/p&gt;

&lt;p&gt;Agents reviewed them before sending.&lt;/p&gt;

&lt;p&gt;That worked reasonably well.&lt;/p&gt;

&lt;p&gt;Then came the obvious question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If agents approve most responses, why not send them automatically?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is where the risk changed.&lt;/p&gt;

&lt;p&gt;A draft can be imperfect.&lt;/p&gt;

&lt;p&gt;An automatically sent customer response needs a much higher reliability threshold.&lt;/p&gt;

&lt;p&gt;The model occasionally misunderstood account context.&lt;/p&gt;

&lt;p&gt;Sometimes it made promises the support team could not keep.&lt;/p&gt;

&lt;p&gt;Sometimes the wording sounded confident despite incomplete information.&lt;/p&gt;

&lt;p&gt;We tried confidence scoring.&lt;/p&gt;

&lt;p&gt;But what exactly was confidence?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Confidence the model understood the question?

Confidence retrieval found the correct account data?

Confidence the policy was interpreted correctly?

Confidence the response was safe to send?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One number could not represent all of those things.&lt;/p&gt;

&lt;p&gt;OpenAI's current evaluation guidance makes a similar broader point: teams need to define what success means and test against representative real-world conditions rather than relying on vague quality judgments.&lt;/p&gt;

&lt;p&gt;We removed auto-send.&lt;/p&gt;

&lt;p&gt;The draft assistant survived.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;Human review is not always temporary scaffolding.&lt;/p&gt;

&lt;p&gt;Sometimes it is the correct product architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. AI-Generated Dashboard Insights
&lt;/h2&gt;

&lt;p&gt;This feature sounded excellent in planning meetings.&lt;/p&gt;

&lt;p&gt;Instead of showing users a chart, we would explain the chart.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revenue decreased 14% this month.

Possible explanation:
Customer churn increased in the enterprise segment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users loved the idea.&lt;/p&gt;

&lt;p&gt;The problem was the phrase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Possible explanation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model was excellent at producing plausible narratives.&lt;/p&gt;

&lt;p&gt;But dashboards mostly contained observational data.&lt;/p&gt;

&lt;p&gt;A change happening beside another change did not mean one caused the other.&lt;/p&gt;

&lt;p&gt;The feature could turn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metric A decreased.

Metric B increased.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metric A decreased because Metric B increased.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sentence sounded useful.&lt;/p&gt;

&lt;p&gt;It was often unsupported.&lt;/p&gt;

&lt;p&gt;We changed the system so the model could only describe facts visible in the data.&lt;/p&gt;

&lt;p&gt;That produced safer outputs such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revenue decreased 14%.

Enterprise churn increased from 3.2% to 4.1%
during the same period.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better.&lt;/p&gt;

&lt;p&gt;But users told us something unexpected.&lt;/p&gt;

&lt;p&gt;They could already see those numbers on the dashboard.&lt;/p&gt;

&lt;p&gt;We had built a sophisticated system to narrate information that was already visible.&lt;/p&gt;

&lt;p&gt;The AI was no longer dangerous.&lt;/p&gt;

&lt;p&gt;It was simply unnecessary.&lt;/p&gt;

&lt;p&gt;So we removed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;Reducing risk can sometimes reduce an AI feature until very little unique value remains.&lt;/p&gt;

&lt;p&gt;That is useful information.&lt;/p&gt;

&lt;p&gt;Delete it.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Universal AI Assistant
&lt;/h2&gt;

&lt;p&gt;This was our most ambitious feature.&lt;/p&gt;

&lt;p&gt;A floating assistant appeared throughout the application.&lt;/p&gt;

&lt;p&gt;Users could ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Find this customer."

"Explain this report."

"Create a ticket."

"Summarize these records."

"Update this status."

"Where is this setting?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One assistant.&lt;/p&gt;

&lt;p&gt;Everything connected.&lt;/p&gt;

&lt;p&gt;It looked like the future.&lt;/p&gt;

&lt;p&gt;It also created an enormous engineering surface.&lt;/p&gt;

&lt;p&gt;Every new capability required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool definition
Permissions
Prompt updates
Evaluation cases
Error handling
Observability
Security review
UI behavior
Fallback logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Latency also became inconsistent.&lt;/p&gt;

&lt;p&gt;A simple question might require one model call.&lt;/p&gt;

&lt;p&gt;Another request might need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
 ↓
Tool call
 ↓
Database
 ↓
LLM
 ↓
Second tool
 ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then came permissions.&lt;/p&gt;

&lt;p&gt;The assistant needed to understand not only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this tool update invoices?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;but also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this particular user update this particular invoice in this particular state?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eventually, we looked at usage.&lt;/p&gt;

&lt;p&gt;Users were not asking for everything.&lt;/p&gt;

&lt;p&gt;Most requests clustered around a few narrow workflows.&lt;/p&gt;

&lt;p&gt;We replaced the universal assistant with focused AI features inside those workflows.&lt;/p&gt;

&lt;p&gt;Less magical.&lt;/p&gt;

&lt;p&gt;Much easier to understand.&lt;/p&gt;

&lt;p&gt;Much easier to test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;p&gt;A narrow AI feature that solves one frequent problem can be more valuable than a universal assistant capable of doing twenty things.&lt;/p&gt;




&lt;h2&gt;
  
  
  What These Failures Had in Common
&lt;/h2&gt;

&lt;p&gt;At first, we blamed different technical problems.&lt;/p&gt;

&lt;p&gt;Search had retrieval issues.&lt;/p&gt;

&lt;p&gt;Code review had noise.&lt;/p&gt;

&lt;p&gt;Support had reliability problems.&lt;/p&gt;

&lt;p&gt;Analytics had grounding problems.&lt;/p&gt;

&lt;p&gt;The assistant had complexity problems.&lt;/p&gt;

&lt;p&gt;But underneath them was the same mistake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We started with what the model could do instead of what the user actually needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The early question had been:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can AI do this?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The better questions became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does this problem need AI?

Is AI better than the existing workflow?

How will we measure success?

What happens when the answer is wrong?

Will users understand the uncertainty?

Does the value justify the operational cost?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changed our feature-development process more than any prompt technique did.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Changed How We Evaluate AI Features
&lt;/h2&gt;

&lt;p&gt;We no longer consider a convincing prototype strong evidence.&lt;/p&gt;

&lt;p&gt;A prototype tells us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not tell us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This should become a product feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we define the evaluation before building too far.&lt;/p&gt;

&lt;p&gt;For example, an AI support feature might need to improve response time without increasing incorrect policy answers.&lt;/p&gt;

&lt;p&gt;An AI review feature might need to detect meaningful defects without producing excessive low-value comments.&lt;/p&gt;

&lt;p&gt;An AI search feature might need answers that are both correct and supported by retrieved evidence.&lt;/p&gt;

&lt;p&gt;A strong &lt;a href="https://sdlccorp.com/ai-consulting-company/" rel="noopener noreferrer"&gt;AI consulting process&lt;/a&gt; should define the use case, success metrics, validation approach, and business value before a prototype becomes a production feature.&lt;br&gt;
This fits well because SDLC Corp’s AI consulting page specifically covers use-case planning, model validation, explainability, and deployment strategy.&lt;/p&gt;

&lt;p&gt;This reflects a broader principle behind modern AI evaluation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Specify the desired behavior, measure it on representative cases, and use failures to drive improvement.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Deleting the Feature Was Sometimes the Best Result
&lt;/h2&gt;

&lt;p&gt;Engineering teams naturally dislike deleting work.&lt;/p&gt;

&lt;p&gt;A feature may have taken weeks.&lt;/p&gt;

&lt;p&gt;The architecture may be elegant.&lt;/p&gt;

&lt;p&gt;The prompt may have gone through twenty versions.&lt;/p&gt;

&lt;p&gt;The evaluation harness may be excellent.&lt;/p&gt;

&lt;p&gt;None of that makes the feature valuable.&lt;/p&gt;

&lt;p&gt;One of the healthiest decisions an AI team can make is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This works, but it should not exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deleting a feature does not necessarily mean the experiment failed.&lt;/p&gt;

&lt;p&gt;Sometimes the experiment answered the most important question.&lt;/p&gt;

&lt;p&gt;And the answer was &lt;strong&gt;no&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;Our biggest &lt;strong&gt;AI feature failure&lt;/strong&gt; was not hallucination.&lt;/p&gt;

&lt;p&gt;It was confusing capability with usefulness.&lt;/p&gt;

&lt;p&gt;Modern models can summarize, classify, generate, search, call tools, and interpret large amounts of information.&lt;/p&gt;

&lt;p&gt;That makes it surprisingly easy to create impressive prototypes.&lt;/p&gt;

&lt;p&gt;The difficult part is deciding which capabilities deserve to become permanent product features.&lt;/p&gt;

&lt;p&gt;The standard became higher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Not:
Can the model do this?

But:
Does this make the product meaningfully better?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That question deleted five features.&lt;/p&gt;

&lt;p&gt;It also improved the ones we kept.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;If you are building AI features, do not only keep a list of what shipped.&lt;/p&gt;

&lt;p&gt;Keep another list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What did we test?

What failed?

What did users ignore?

What produced too much risk?

What was technically successful but unnecessary?

What did we delete?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those decisions are part of AI engineering too.&lt;/p&gt;

&lt;p&gt;Sometimes the best evidence that an AI team is learning is not the number of features it launches.&lt;/p&gt;

&lt;p&gt;It is the number it is willing to remove.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>What Broke When We Moved From GPT to Self-Hosted</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Thu, 03 Sep 2026 05:37:10 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/what-broke-when-we-moved-from-gpt-to-self-hosted-32c1</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/what-broke-when-we-moved-from-gpt-to-self-hosted-32c1</guid>
      <description>&lt;p&gt;Moving from a hosted GPT API to a self-hosted model sounded straightforward.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4zyrynrl1uq4ftmv7fvh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4zyrynrl1uq4ftmv7fvh.png" alt="Moved From GPT to Self-Hosted" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application already had one clean interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt
  ↓
LLM API
  ↓
Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the migration plan looked simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Replace API endpoint
        ↓
Change model name
        ↓
Deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint worked.&lt;/p&gt;

&lt;p&gt;The model answered.&lt;/p&gt;

&lt;p&gt;And then almost everything around it started behaving differently.&lt;/p&gt;

&lt;p&gt;The lesson from our &lt;strong&gt;self hosted LLM migration&lt;/strong&gt; was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Replacing the model was easy. Replacing the behavior we had built around the model was not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a representative engineering post-mortem. The failures and architecture below illustrate common migration problems rather than a specific customer deployment.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why We Wanted to Self-Host
&lt;/h2&gt;

&lt;p&gt;The reasons were reasonable.&lt;/p&gt;

&lt;p&gt;We wanted more control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model selection&lt;/li&gt;
&lt;li&gt;Infrastructure&lt;/li&gt;
&lt;li&gt;Deployment location&lt;/li&gt;
&lt;li&gt;Data handling&lt;/li&gt;
&lt;li&gt;Inference configuration&lt;/li&gt;
&lt;li&gt;Capacity planning&lt;/li&gt;
&lt;li&gt;Model upgrades&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also wanted the ability to optimize around our own workload instead of treating inference as an external black box.&lt;/p&gt;

&lt;p&gt;So we introduced a self-hosted inference server behind the existing application.&lt;/p&gt;

&lt;p&gt;Conceptually, the architecture changed from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before

Application
    ↓
Hosted GPT API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;After

Application
    ↓
LLM Gateway
    ↓
Self-Hosted Model Server
    ↓
GPU Infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API looked familiar.&lt;/p&gt;

&lt;p&gt;That created false confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #1: API Compatibility Was Not Behavior Compatibility
&lt;/h2&gt;

&lt;p&gt;One of our first assumptions was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the server supports an OpenAI-compatible API, our application should behave the same.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the HTTP level, that was mostly true.&lt;/p&gt;

&lt;p&gt;For example, servers such as vLLM expose OpenAI-compatible Chat Completions, Responses, embeddings, and other endpoints.&lt;/p&gt;

&lt;p&gt;But receiving a familiar response structure such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not mean two models will interpret the same conversation identically.&lt;/p&gt;

&lt;p&gt;We saw changes in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instruction following&lt;/li&gt;
&lt;li&gt;Response length&lt;/li&gt;
&lt;li&gt;Formatting&lt;/li&gt;
&lt;li&gt;Refusal behavior&lt;/li&gt;
&lt;li&gt;JSON reliability&lt;/li&gt;
&lt;li&gt;Tool selection&lt;/li&gt;
&lt;li&gt;Edge-case reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration compiled.&lt;/p&gt;

&lt;p&gt;The product behavior changed.&lt;/p&gt;

&lt;p&gt;That was our first major mistake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We tested protocol compatibility before behavioral compatibility.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break #2: Our Prompts Were More Model-Specific Than We Thought
&lt;/h2&gt;

&lt;p&gt;We had system prompts that had been tuned over months.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Return a concise answer.
Do not include commentary.
Use the supplied context only.
Return JSON matching the requested format.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They worked consistently enough with the previous model that we started treating them as application logic.&lt;/p&gt;

&lt;p&gt;Then we moved models.&lt;/p&gt;

&lt;p&gt;Some responses became longer.&lt;/p&gt;

&lt;p&gt;Some ignored preferred formatting.&lt;/p&gt;

&lt;p&gt;Some interpreted instructions differently.&lt;/p&gt;

&lt;p&gt;Others needed clearer examples.&lt;/p&gt;

&lt;p&gt;The prompts were not really portable.&lt;/p&gt;

&lt;p&gt;They were part of the model integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then We Discovered Chat Templates
&lt;/h2&gt;

&lt;p&gt;With many self-hosted chat models, the messages sent by the application eventually need to become a token sequence such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;system&amp;gt;
You are...
&amp;lt;user&amp;gt;
...
&amp;lt;assistant&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But different models may expect different special tokens and formatting.&lt;/p&gt;

&lt;p&gt;Hugging Face explicitly recommends using the chat template associated with the model because formatting that does not match training can hurt performance.&lt;/p&gt;

&lt;p&gt;That meant our architecture was actually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application messages
        ↓
Chat template
        ↓
Tokenizer
        ↓
Model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;not simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application messages
        ↓
Model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A wrong template could produce valid inference with worse behavior.&lt;/p&gt;

&lt;p&gt;That is a particularly dangerous failure because nothing crashes.&lt;/p&gt;

&lt;p&gt;The model just becomes less reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #3: Structured Output Became Our Problem
&lt;/h2&gt;

&lt;p&gt;Several application workflows expected objects like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requires_human"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the hosted implementation, we relied heavily on schema-constrained responses.&lt;/p&gt;

&lt;p&gt;OpenAI supports Structured Outputs and strict function schemas designed to make responses conform to supplied schemas.&lt;/p&gt;

&lt;p&gt;Our first self-hosted implementation mostly relied on prompting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Respond with valid JSON only.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked.&lt;/p&gt;

&lt;p&gt;Until it did not.&lt;/p&gt;

&lt;p&gt;We received responses such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sure! Here's the JSON:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;followed by JSON.&lt;/p&gt;

&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when the schema expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Occasionally, we also received incomplete objects.&lt;/p&gt;

&lt;p&gt;The migration exposed an architectural mistake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Parsing model output is not validation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  We Added Schema Enforcement
&lt;/h2&gt;

&lt;p&gt;Our gateway eventually owned structured-output validation.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model output
    ↓
Schema validation
    ↓
Valid?
 ┌──┴──┐
Yes    No
 ↓      ↓
Return Retry / Fail safely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern inference servers can also support constrained or structured generation.&lt;/p&gt;

&lt;p&gt;vLLM, for example, supports JSON Schema, regex, grammar, and other structured-output constraints.&lt;/p&gt;

&lt;p&gt;That helped enormously.&lt;/p&gt;

&lt;p&gt;But it also reinforced the larger lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The hosted platform had been providing more than tokens. It had been providing behavioral infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break #4: Tool Calling Changed
&lt;/h2&gt;

&lt;p&gt;Our application used tools for actions such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_documents()
lookup_customer()
create_ticket()
get_order_status()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before migration, we had logic built around a fairly predictable tool-call structure.&lt;/p&gt;

&lt;p&gt;After migration, three things changed.&lt;/p&gt;

&lt;p&gt;Sometimes the new model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answered directly instead of calling the tool&lt;/li&gt;
&lt;li&gt;Chose the wrong tool&lt;/li&gt;
&lt;li&gt;Produced arguments that were technically valid but semantically poor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool calling is partly an inference-server feature, but it is also heavily model-dependent.&lt;/p&gt;

&lt;p&gt;For example, vLLM requires model-specific tool parsers and configurations for automatic tool selection in many setups.&lt;/p&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tools=[...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;was not enough.&lt;/p&gt;

&lt;p&gt;We had to test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should the model call a tool?

Which tool should it choose?

Are the arguments correct?

What happens after the tool output returns?

Can it handle multiple tool steps?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool-use quality became its own evaluation suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #5: Latency Looked Great Until Concurrency Arrived
&lt;/h2&gt;

&lt;p&gt;Our first benchmark was encouraging.&lt;/p&gt;

&lt;p&gt;One request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Model
 ↓
Fast response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then production-like traffic arrived.&lt;/p&gt;

&lt;p&gt;Now it looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request ─┐
Request ─┤
Request ─┤
Request ─┼──→ GPU
Request ─┤
Request ─┤
Request ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly we were debugging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queue time&lt;/li&gt;
&lt;li&gt;Batching&lt;/li&gt;
&lt;li&gt;Token throughput&lt;/li&gt;
&lt;li&gt;Long prompts&lt;/li&gt;
&lt;li&gt;Long generations&lt;/li&gt;
&lt;li&gt;Memory pressure&lt;/li&gt;
&lt;li&gt;Concurrent requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Single-request latency had told us almost nothing about production capacity.&lt;/p&gt;

&lt;p&gt;The important metrics became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time to First Token
Tokens per Second
Queue Time
Requests per Second
P95 Latency
P99 Latency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A self-hosted model is not just a model.&lt;/p&gt;

&lt;p&gt;It is a serving system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #6: Context Length Became a Capacity Problem
&lt;/h2&gt;

&lt;p&gt;Our application frequently supplied:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System instructions&lt;/li&gt;
&lt;li&gt;Conversation history&lt;/li&gt;
&lt;li&gt;Retrieved documents&lt;/li&gt;
&lt;li&gt;Tool definitions&lt;/li&gt;
&lt;li&gt;User input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A request could become large quickly.&lt;/p&gt;

&lt;p&gt;Under a hosted API, we mainly thought about context as a model limit and usage cost.&lt;/p&gt;

&lt;p&gt;Self-hosting added another concern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory capacity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Longer contexts meant more inference memory and lower effective concurrency.&lt;/p&gt;

&lt;p&gt;So a request that worked perfectly alone could reduce throughput when many users arrived together.&lt;/p&gt;

&lt;p&gt;We started treating context as an infrastructure resource.&lt;/p&gt;

&lt;p&gt;Instead of automatically sending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Entire conversation
+
10 retrieved documents
+
All tool definitions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we became much stricter about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document selection&lt;/li&gt;
&lt;li&gt;History trimming&lt;/li&gt;
&lt;li&gt;Prompt size&lt;/li&gt;
&lt;li&gt;Tool exposure&lt;/li&gt;
&lt;li&gt;Output limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prompt optimization became capacity optimization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #7: Retries Became More Dangerous
&lt;/h2&gt;

&lt;p&gt;Our hosted integration had retry logic.&lt;/p&gt;

&lt;p&gt;Something failed?&lt;/p&gt;

&lt;p&gt;Retry.&lt;/p&gt;

&lt;p&gt;That behavior became dangerous when infrastructure was already overloaded.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server overloaded
      ↓
Request times out
      ↓
Client retries
      ↓
More load
      ↓
More timeouts
      ↓
More retries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we had created a retry storm.&lt;/p&gt;

&lt;p&gt;We changed the gateway to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bounded retries&lt;/li&gt;
&lt;li&gt;Backoff&lt;/li&gt;
&lt;li&gt;Timeouts&lt;/li&gt;
&lt;li&gt;Concurrency limits&lt;/li&gt;
&lt;li&gt;Overload handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inference failure had become distributed-systems failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Break #8: We Became the Operations Team
&lt;/h2&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST request
     ↓
Receive result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model weights
GPU drivers
Inference server
Container images
Autoscaling
Networking
Load balancing
Monitoring
Logging
Model versions
Security
Capacity planning
Failure recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those are reasons not to self-host.&lt;/p&gt;

&lt;p&gt;They are simply costs that need to exist in the migration plan.&lt;/p&gt;

&lt;p&gt;We had moved responsibility across a boundary.&lt;/p&gt;

&lt;p&gt;The managed API provider was no longer operating inference for us.&lt;/p&gt;

&lt;p&gt;Self-hosting also turns model deployment into a &lt;a href="https://sdlccorp.com/cloud-application-development-services/" rel="noopener noreferrer"&gt;cloud infrastructure and scaling&lt;/a&gt; problem, because GPU capacity, monitoring, networking, and failure recovery now belong to your team.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Important Fix: Put a Gateway in Front
&lt;/h2&gt;

&lt;p&gt;Initially, parts of the application called the inference server directly.&lt;/p&gt;

&lt;p&gt;That made every model difference leak into product code.&lt;/p&gt;

&lt;p&gt;We replaced that design with an internal LLM gateway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
     ↓
LLM Gateway
     ↓
 ┌───────────────┐
 │ Hosted Model  │
 │ Self-Hosted A │
 │ Self-Hosted B │
 └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway owned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request normalization&lt;/li&gt;
&lt;li&gt;Prompt versions&lt;/li&gt;
&lt;li&gt;Timeouts&lt;/li&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Structured-output validation&lt;/li&gt;
&lt;li&gt;Tool schemas&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Metrics&lt;/li&gt;
&lt;li&gt;Model routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Application code stopped caring which inference backend produced the answer.&lt;/p&gt;

&lt;p&gt;A stable gateway also makes &lt;a href="https://sdlccorp.com/enterprise-ai-development-company/" rel="noopener noreferrer"&gt;enterprise AI development&lt;/a&gt; easier because applications can switch models, validate outputs, monitor behavior, and scale inference without coupling every workflow to one provider.&lt;/p&gt;

&lt;p&gt;That made the migration much safer.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Also Stopped Doing Big-Bang Model Swaps
&lt;/h2&gt;

&lt;p&gt;Our first approach was basically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPT
 ↓
New model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later migrations used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current model
      ↓
Shadow traffic
      ↓
Evaluation
      ↓
Small percentage
      ↓
Compare
      ↓
Increase traffic
      ↓
Full migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new model initially received traffic without controlling the user-visible response.&lt;/p&gt;

&lt;p&gt;We compared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answer quality&lt;/li&gt;
&lt;li&gt;JSON validity&lt;/li&gt;
&lt;li&gt;Tool selection&lt;/li&gt;
&lt;li&gt;Latency&lt;/li&gt;
&lt;li&gt;Token usage&lt;/li&gt;
&lt;li&gt;Failure rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That surfaced problems before customers saw them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Our Evaluation Suite Became the Real Migration Plan
&lt;/h2&gt;

&lt;p&gt;The most useful artifact was not the deployment script.&lt;/p&gt;

&lt;p&gt;It was our evaluation dataset.&lt;/p&gt;

&lt;p&gt;We collected representative requests for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Simple Q&amp;amp;A
RAG
Extraction
Classification
Tool calling
Long context
Ambiguous instructions
Bad input
Edge cases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every candidate model, we tested the same workload.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Existing Model&lt;/th&gt;
&lt;th&gt;Self-Hosted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON schema compliance&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool selection&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG answer quality&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-context request&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;SLOW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Classification&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now model selection was based on product behavior.&lt;/p&gt;

&lt;p&gt;Not leaderboard scores.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke
&lt;/h2&gt;

&lt;p&gt;Looking back, the failures fell into a few categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Behavior
&lt;/h3&gt;

&lt;p&gt;Prompts that worked well with one model needed adjustment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatting
&lt;/h3&gt;

&lt;p&gt;Structured output required stronger enforcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Use
&lt;/h3&gt;

&lt;p&gt;Tool selection and arguments needed fresh evaluation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;Single-request benchmarks hid concurrency problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;Large prompts reduced serving capacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reliability
&lt;/h3&gt;

&lt;p&gt;Retries, overload, and failover became our responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operations
&lt;/h3&gt;

&lt;p&gt;Inference infrastructure became part of the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Worked
&lt;/h2&gt;

&lt;p&gt;Several changes made the migration much safer.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Stable Internal API
&lt;/h3&gt;

&lt;p&gt;Application code talked to our gateway, not directly to a specific model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model-Specific Prompting
&lt;/h3&gt;

&lt;p&gt;We stopped pretending every model needed identical prompts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema Validation
&lt;/h3&gt;

&lt;p&gt;Structured outputs were validated rather than trusted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production-Like Load Tests
&lt;/h3&gt;

&lt;p&gt;We tested concurrent traffic instead of one request at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shadow Evaluation
&lt;/h3&gt;

&lt;p&gt;New models were compared before receiving full traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioned Models and Prompts
&lt;/h3&gt;

&lt;p&gt;Every production request could be traced to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model version
Prompt version
Gateway version
Inference configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That made regressions much easier to investigate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture We Ended Up With
&lt;/h2&gt;

&lt;p&gt;The final system looked closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   Application
                       │
                       ▼
                   LLM Gateway
                       │
       ┌───────────────┼───────────────┐
       ▼               ▼               ▼
 Prompt Manager    Validation     Observability
       │               │               │
       └───────────────┼───────────────┘
                       ▼
                 Model Router
                  ┌────┴────┐
                  ▼         ▼
              Hosted     Self-Hosted
               Model       Models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key abstraction was no longer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;callModel()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;executeLLMWorkflow()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because production AI applications depend on much more than generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Would Do Differently
&lt;/h2&gt;

&lt;p&gt;For the next &lt;strong&gt;self hosted LLM migration&lt;/strong&gt;, we would use this order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Build Evaluations First
&lt;/h3&gt;

&lt;p&gt;Build the evaluation suite before choosing the replacement model.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inventory Every Model-Dependent Feature
&lt;/h3&gt;

&lt;p&gt;Include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured output&lt;/li&gt;
&lt;li&gt;Tools&lt;/li&gt;
&lt;li&gt;RAG&lt;/li&gt;
&lt;li&gt;Vision, if applicable&lt;/li&gt;
&lt;li&gt;Context length&lt;/li&gt;
&lt;li&gt;Streaming&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Introduce an LLM Gateway
&lt;/h3&gt;

&lt;p&gt;Put the gateway in place before changing providers or models.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Validate the Chat Template
&lt;/h3&gt;

&lt;p&gt;Make sure the model receives messages in the format it was trained to understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Load-Test Realistic Prompts
&lt;/h3&gt;

&lt;p&gt;Do not rely only on tiny benchmark requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Test Tool Behavior Separately
&lt;/h3&gt;

&lt;p&gt;API support does not prove that the model uses tools correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Run Shadow Traffic
&lt;/h3&gt;

&lt;p&gt;Compare production workloads before switching user-visible responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Plan Operations Before Launch
&lt;/h3&gt;

&lt;p&gt;GPU capacity, monitoring, failure handling, scaling, and upgrades belong in the original migration scope.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;We originally thought we were replacing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Self-hosted model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we were really replacing was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model
+
Serving infrastructure
+
Structured output behavior
+
Tool behavior
+
Scaling
+
Reliability
+
Operational responsibility
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much larger migration.&lt;/p&gt;

&lt;p&gt;And it explains why an application can successfully return its first self-hosted response while still being nowhere near production-ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;self hosted LLM migration&lt;/strong&gt; should not begin with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which open model looks closest to our current GPT model?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What behavior does our application depend on?

How will we measure that behavior?

Which parts are currently handled by the provider?

Which of those responsibilities are moving to us?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then migrate in layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inventory dependencies
       ↓
Build evaluations
       ↓
Add gateway
       ↓
Deploy model
       ↓
Validate outputs
       ↓
Load test
       ↓
Shadow traffic
       ↓
Gradual rollout
       ↓
Monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-hosting gave us more control.&lt;/p&gt;

&lt;p&gt;But that control came with responsibility.&lt;/p&gt;

&lt;p&gt;The real migration was not from one model to another.&lt;/p&gt;

&lt;p&gt;It was from &lt;strong&gt;consuming inference as a service to operating inference as a production system&lt;/strong&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Post-Mortem: The Go-Live That Slipped Six Weeks</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:09:43 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/post-mortem-the-go-live-that-slipped-six-weeks-595j</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/post-mortem-the-go-live-that-slipped-six-weeks-595j</guid>
      <description>&lt;p&gt;The ERP project looked close to finished.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o32b6jvta31gq73tguu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o32b6jvta31gq73tguu.png" alt="Post-Mortem: The Go-Live That Slipped Six Weeks" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Development was done. Integrations were connected. Most users had completed training. The production environment was ready.&lt;/p&gt;

&lt;p&gt;Then we ran the final readiness review.&lt;/p&gt;

&lt;p&gt;The go-live moved by &lt;strong&gt;six weeks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not because of one catastrophic bug.&lt;/p&gt;

&lt;p&gt;Because several small risks we had accepted for months finally became impossible to ignore.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a representative ERP implementation post-mortem. The timeline and examples are illustrative, not customer production data.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Original Plan
&lt;/h2&gt;

&lt;p&gt;The final weeks looked simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Finish UAT
   ↓
Final data migration
   ↓
Cutover rehearsal
   ↓
Business sign-off
   ↓
Go live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But each step depended on the one before it actually being complete.&lt;/p&gt;

&lt;p&gt;That was our first mistake.&lt;/p&gt;

&lt;p&gt;We planned around dates.&lt;/p&gt;

&lt;p&gt;We should have planned around &lt;strong&gt;exit criteria&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #1: UAT Was “Almost Done”
&lt;/h2&gt;

&lt;p&gt;The dashboard showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UAT: 92% complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounded good.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;p&gt;The missing 8% included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;month-end finance scenarios,&lt;/li&gt;
&lt;li&gt;partial order cancellations,&lt;/li&gt;
&lt;li&gt;inventory adjustments,&lt;/li&gt;
&lt;li&gt;credit processing,&lt;/li&gt;
&lt;li&gt;exception workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These were not edge cases we could safely ignore.&lt;/p&gt;

&lt;p&gt;They were business processes.&lt;/p&gt;

&lt;p&gt;The lesson was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A test cycle is not complete because most test cases passed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is complete when critical business processes are tested, defects are resolved or formally accepted, and the business signs off.&lt;/p&gt;

&lt;p&gt;Current ERP implementation guidance similarly treats UAT completion and stakeholder approval as explicit go-live readiness conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #2: The Data Migration Did Not Fit the Cutover Window
&lt;/h2&gt;

&lt;p&gt;Our migration scripts worked.&lt;/p&gt;

&lt;p&gt;That was not the problem.&lt;/p&gt;

&lt;p&gt;The problem was time.&lt;/p&gt;

&lt;p&gt;The cutover window allowed roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 hours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our latest migration rehearsal took:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;14 hours 40 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically successful.&lt;/p&gt;

&lt;p&gt;Operationally unusable.&lt;/p&gt;

&lt;p&gt;We found several causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow transformation jobs,&lt;/li&gt;
&lt;li&gt;unnecessary historical records,&lt;/li&gt;
&lt;li&gt;sequential loads that could be reorganized,&lt;/li&gt;
&lt;li&gt;reconciliation happening too late,&lt;/li&gt;
&lt;li&gt;failed records requiring manual investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next dry runs became timed rehearsals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract
   ↓
Transform
   ↓
Load
   ↓
Reconcile
   ↓
Business validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A migration is not ready because it works.&lt;/p&gt;

&lt;p&gt;It must also work &lt;strong&gt;inside the real cutover window&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A strong &lt;a href="https://sdlccorp.com/post/erp-data-migration-strategy-best-practices/" rel="noopener noreferrer"&gt;ERP data migration strategy&lt;/a&gt; should include repeated trial loads, reconciliation, realistic-volume testing, cutover timing, and clear rollback criteria.&lt;/p&gt;

&lt;p&gt;Repeated migration rehearsals and validation are specifically recommended before production cutover.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #3: Integrations Passed Functional Tests but Failed at Volume
&lt;/h2&gt;

&lt;p&gt;One integration worked perfectly when we sent ten transactions.&lt;/p&gt;

&lt;p&gt;Then we tested production-like traffic.&lt;/p&gt;

&lt;p&gt;Queues started growing.&lt;/p&gt;

&lt;p&gt;Retries increased.&lt;/p&gt;

&lt;p&gt;One downstream service slowed the entire chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERP
 ↓
Integration Layer
 ↓
Warehouse
 ↓
Carrier
 ↓
ERP confirmation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The happy path worked.&lt;/p&gt;

&lt;p&gt;The production workload did not.&lt;/p&gt;

&lt;p&gt;This is why &lt;a href="https://sdlccorp.com/erp/consulting/" rel="noopener noreferrer"&gt;ERP system integration&lt;/a&gt; testing needs to cover realistic transaction volumes, queue behavior, retries, external dependencies, and failure recovery—not just basic connectivity.&lt;/p&gt;

&lt;p&gt;We had tested:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does the integration work?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We had not tested enough:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does it still work when hundreds or thousands of transactions arrive together?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference contributed directly to the &lt;strong&gt;ERP go live delay&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Integration testing should include realistic peak volumes and external-system failure scenarios, not only basic connectivity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #4: Data Reconciliation Was Still Manual
&lt;/h2&gt;

&lt;p&gt;Our migration technically loaded the expected number of records.&lt;/p&gt;

&lt;p&gt;But record count was not enough.&lt;/p&gt;

&lt;p&gt;For inventory, finance, and open transactions, we needed to prove:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
  =
Target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;at the correct business level.&lt;/p&gt;

&lt;p&gt;That meant comparing things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inventory quantity by SKU and location,&lt;/li&gt;
&lt;li&gt;open purchase order value,&lt;/li&gt;
&lt;li&gt;customer balances,&lt;/li&gt;
&lt;li&gt;vendor balances,&lt;/li&gt;
&lt;li&gt;journal totals,&lt;/li&gt;
&lt;li&gt;open sales orders.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A migration could contain the correct number of records and still contain incorrect values.&lt;/p&gt;

&lt;p&gt;We automated more of the reconciliation and turned unexplained differences into blockers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #5: The Cutover Plan Had Tasks but Not Enough Decisions
&lt;/h2&gt;

&lt;p&gt;Our first cutover plan looked detailed.&lt;/p&gt;

&lt;p&gt;It had timestamps.&lt;/p&gt;

&lt;p&gt;Owners.&lt;/p&gt;

&lt;p&gt;Tasks.&lt;/p&gt;

&lt;p&gt;Dependencies.&lt;/p&gt;

&lt;p&gt;But it was missing something critical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;decision points.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;02:00 — Final migration complete
02:30 — Reconciliation
03:00 — Integration validation
03:30 — Business verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what happened if reconciliation failed?&lt;/p&gt;

&lt;p&gt;Who decided whether to continue?&lt;/p&gt;

&lt;p&gt;How much variance was acceptable?&lt;/p&gt;

&lt;p&gt;When did rollback become mandatory?&lt;/p&gt;

&lt;p&gt;The improved runbook included explicit gates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migration complete?
      ↓
Data reconciled?
      ↓
Critical integrations healthy?
      ↓
Business processes validated?
      ↓
GO / NO-GO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cutover runbook needs verification, sign-off, roles, dependencies, and rollback criteria—not just a task list.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why It Became Six Weeks
&lt;/h2&gt;

&lt;p&gt;No single issue required six weeks.&lt;/p&gt;

&lt;p&gt;The combination did.&lt;/p&gt;

&lt;p&gt;The additional time went into roughly four areas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Work Required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UAT&lt;/td&gt;
&lt;td&gt;Finish critical scenarios and resolve blockers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Optimize migration and repeat reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrations&lt;/td&gt;
&lt;td&gt;Test at production-like volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cutover&lt;/td&gt;
&lt;td&gt;Rehearse the complete runbook again&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We also used the time to finish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;production support procedures,&lt;/li&gt;
&lt;li&gt;user access validation,&lt;/li&gt;
&lt;li&gt;monitoring,&lt;/li&gt;
&lt;li&gt;rollback planning,&lt;/li&gt;
&lt;li&gt;business communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The delay was painful.&lt;/p&gt;

&lt;p&gt;But going live with unresolved issues would have been worse.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Second Cutover Rehearsal Changed Everything
&lt;/h2&gt;

&lt;p&gt;We treated the next mock cutover like production.&lt;/p&gt;

&lt;p&gt;Same sequence.&lt;/p&gt;

&lt;p&gt;Similar data volume.&lt;/p&gt;

&lt;p&gt;Same teams.&lt;/p&gt;

&lt;p&gt;Same dependencies.&lt;/p&gt;

&lt;p&gt;Same validation steps.&lt;/p&gt;

&lt;p&gt;The result looked very different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migration within window       PASS
Reconciliation                PASS
Critical integrations         PASS
UAT blockers                  0
Production access             PASS
Support team ready            PASS
Rollback procedure            VERIFIED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only then did the date become credible.&lt;/p&gt;

&lt;p&gt;Mock cutovers are valuable precisely because they test the whole transition rather than individual technical components in isolation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Got Wrong
&lt;/h2&gt;

&lt;p&gt;The biggest mistakes were not deeply technical.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Treated the Go-Live Date as Fixed
&lt;/h3&gt;

&lt;p&gt;The date became more important than readiness.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Counted Progress Instead of Risk
&lt;/h3&gt;

&lt;p&gt;“92% UAT complete” sounded reassuring while critical scenarios were still untested.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Tested Components Individually
&lt;/h3&gt;

&lt;p&gt;Migration worked.&lt;/p&gt;

&lt;p&gt;Integrations worked.&lt;/p&gt;

&lt;p&gt;ERP worked.&lt;/p&gt;

&lt;p&gt;But we had not rehearsed the complete operating system enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Accepted “Almost Ready”
&lt;/h3&gt;

&lt;p&gt;Almost ready multiplied across five workstreams becomes very far from ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;For the second attempt, go-live required explicit evidence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UAT signed off
      ✓

Migration fits window
      ✓

Data reconciled
      ✓

Integrations tested at volume
      ✓

Critical defects closed
      ✓

Users and security validated
      ✓

Support team ready
      ✓

Cutover rehearsed
      ✓

Rollback tested
      ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No green check meant no go-live.&lt;/p&gt;

&lt;p&gt;That made discussions much easier.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do we feel ready?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which exit criterion is still open?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The six-week delay was not created during the final readiness review.&lt;/p&gt;

&lt;p&gt;It was created gradually.&lt;/p&gt;

&lt;p&gt;Every time we said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We'll finish that later.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we borrowed time from cutover.&lt;/p&gt;

&lt;p&gt;Every incomplete test.&lt;/p&gt;

&lt;p&gt;Every unresolved data issue.&lt;/p&gt;

&lt;p&gt;Every integration assumption.&lt;/p&gt;

&lt;p&gt;Every undocumented rollback decision.&lt;/p&gt;

&lt;p&gt;Eventually, all of that debt became visible at once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;ERP go live delay&lt;/strong&gt; is rarely caused by a single dramatic failure.&lt;/p&gt;

&lt;p&gt;More often, it is the result of several workstreams reaching the finish line at &lt;strong&gt;90–95% readiness&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And in an ERP implementation, 95% ready can still mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Finance cannot close.
Inventory does not reconcile.
Orders cannot ship.
Users cannot access the right functions.
Integrations fail under load.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important lesson from the six-week slip was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Go-live is not a calendar date. It is a readiness state.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Set measurable exit criteria.&lt;/p&gt;

&lt;p&gt;Rehearse the full cutover.&lt;/p&gt;

&lt;p&gt;Test with realistic data and volume.&lt;/p&gt;

&lt;p&gt;Make unresolved critical risks visible.&lt;/p&gt;

&lt;p&gt;And let readiness determine the date—not the other way around.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Reconciling Inventory Across Three Systems During Cutover</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Wed, 02 Sep 2026 05:55:38 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/reconciling-inventory-across-three-systems-during-cutover-20lk</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/reconciling-inventory-across-three-systems-during-cutover-20lk</guid>
      <description>&lt;p&gt;The ERP migration looked ready. Customers, products, and purchase orders were loaded, and the interfaces were green.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuf011v1im5zh68rx46oj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuf011v1im5zh68rx46oj.png" alt="Reconciling Inventory Across Three Systems During Cutover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we compared inventory across three systems—and the numbers did not match.&lt;/p&gt;

&lt;p&gt;That was when the real ERP cutover problem began.&lt;/p&gt;

&lt;p&gt;Three systems gave us three different answers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Legacy ERP:     184,732 units
WMS:            183,941 units
New ERP:        185,106 units
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every number was technically explainable.&lt;/p&gt;

&lt;p&gt;None was acceptable for go-live.&lt;/p&gt;

&lt;p&gt;That became the most important lesson from the &lt;strong&gt;ERP cutover&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Inventory reconciliation is not about proving that one total matches. It is about proving that every system agrees on what each quantity means at the exact moment ownership changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is how we approached the problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a representative engineering post-mortem. The systems, quantities, and examples are illustrative rather than customer production data.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Three Systems
&lt;/h2&gt;

&lt;p&gt;Our inventory existed across three operational systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Legacy ERP
&lt;/h3&gt;

&lt;p&gt;The financial system of record.&lt;/p&gt;

&lt;p&gt;It knew about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;item balances,&lt;/li&gt;
&lt;li&gt;inventory valuation,&lt;/li&gt;
&lt;li&gt;purchase orders,&lt;/li&gt;
&lt;li&gt;transfers,&lt;/li&gt;
&lt;li&gt;adjustments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Warehouse Management System
&lt;/h3&gt;

&lt;p&gt;The operational warehouse view.&lt;/p&gt;

&lt;p&gt;It knew exactly where stock physically existed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warehouse
Bin
Lot
Serial
Inventory status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  New ERP
&lt;/h3&gt;

&lt;p&gt;The system we were preparing to activate.&lt;/p&gt;

&lt;p&gt;It needed a correct opening inventory position before new transactions could start.&lt;/p&gt;

&lt;p&gt;At first, the reconciliation looked straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Legacy ERP
     ↓
Compare
     ↓
WMS
     ↓
Load
     ↓
New ERP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A structured &lt;a href="https://sdlccorp.com/erp/implementation/" rel="noopener noreferrer"&gt;ERP implementation&lt;/a&gt; should include trial migrations, reconciliation, integration testing, business sign-off, and post-go-live monitoring—not just the final data load.&lt;br&gt;
It was not.&lt;/p&gt;


&lt;h2&gt;
  
  
  Inventory Was Not One Number
&lt;/h2&gt;

&lt;p&gt;Our first mistake was comparing total quantity.&lt;/p&gt;

&lt;p&gt;Suppose all three systems reported roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;185,000 units
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looked encouraging.&lt;/p&gt;

&lt;p&gt;But total quantity could hide serious errors.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SKU&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Legacy ERP&lt;/th&gt;
&lt;th&gt;WMS&lt;/th&gt;
&lt;th&gt;New ERP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A100&lt;/td&gt;
&lt;td&gt;Chicago&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A100&lt;/td&gt;
&lt;td&gt;Dallas&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B220&lt;/td&gt;
&lt;td&gt;Chicago&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The grand total still matched.&lt;/p&gt;

&lt;p&gt;Operationally, it was wrong.&lt;/p&gt;

&lt;p&gt;So the reconciliation grain became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKU
+
Warehouse
+
Inventory status
+
Unit of measure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For controlled items, we went deeper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKU
+
Location
+
Lot / Serial
+
Status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A matching grand total was no longer enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Variance: Timing
&lt;/h2&gt;

&lt;p&gt;The biggest source of differences was not bad data.&lt;/p&gt;

&lt;p&gt;It was timing.&lt;/p&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:01  ERP export starts
10:03  Warehouse ships 20 units
10:05  WMS snapshot starts
10:08  ERP receives shipment confirmation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare both systems.&lt;/p&gt;

&lt;p&gt;They represent different moments.&lt;/p&gt;

&lt;p&gt;Neither system is necessarily wrong.&lt;/p&gt;

&lt;p&gt;This is why reconciliation without a defined &lt;strong&gt;cutoff timestamp&lt;/strong&gt; is meaningless.&lt;/p&gt;

&lt;p&gt;We introduced one rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All inventory comparisons must represent
the same business moment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That meant defining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when warehouse transactions stop,&lt;/li&gt;
&lt;li&gt;when ERP posting stops,&lt;/li&gt;
&lt;li&gt;when interfaces pause,&lt;/li&gt;
&lt;li&gt;which transactions belong before cutover,&lt;/li&gt;
&lt;li&gt;which transactions belong after cutover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The timestamp became part of the reconciliation evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Needed a Real Freeze Window
&lt;/h2&gt;

&lt;p&gt;Initially, we hoped interfaces could continue running while we reconciled.&lt;/p&gt;

&lt;p&gt;That created a moving target.&lt;/p&gt;

&lt;p&gt;Every time we fixed one variance, another transaction changed the balance.&lt;/p&gt;

&lt;p&gt;The final cutover sequence became stricter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stop warehouse transactions
        ↓
Pause integrations
        ↓
Complete in-flight messages
        ↓
Freeze legacy inventory
        ↓
Take final snapshots
        ↓
Reconcile
        ↓
Load new ERP
        ↓
Reconcile again
        ↓
Go / No-Go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The freeze was inconvenient.&lt;/p&gt;

&lt;p&gt;But a reconciliation against continuously changing inventory was worse.&lt;/p&gt;

&lt;p&gt;A controlled &lt;a href="https://sdlccorp.com/post/erp-data-migration-strategy-best-practices/" rel="noopener noreferrer"&gt;ERP data migration strategy&lt;/a&gt; needs a defined freeze window, final delta handling, reconciliation rules, and clear go/no-go criteria before production starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Second Variance: Inventory Status
&lt;/h2&gt;

&lt;p&gt;The WMS did not simply know that 100 units existed.&lt;/p&gt;

&lt;p&gt;It knew their status.&lt;/p&gt;

&lt;p&gt;For one SKU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Available:       70
Allocated:       15
Quality Hold:    10
Damaged:          5
-------------------
Physical:       100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The legacy ERP might display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On Hand: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the new ERP imported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Available: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The totals matched.&lt;/p&gt;

&lt;p&gt;The business meaning did not.&lt;/p&gt;

&lt;p&gt;That could allow damaged or quarantined inventory to be sold.&lt;/p&gt;

&lt;p&gt;So we created explicit status mappings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WMS AVAILABLE      → ERP AVAILABLE
WMS QA_HOLD        → ERP QUALITY_HOLD
WMS DAMAGED        → ERP BLOCKED
WMS ALLOCATED      → ERP RESERVED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unknown statuses were not defaulted.&lt;/p&gt;

&lt;p&gt;They became cutover exceptions.&lt;/p&gt;

&lt;p&gt;That decision prevented silent inventory corruption.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Third Variance: Units of Measure
&lt;/h2&gt;

&lt;p&gt;Then we found quantities that were mathematically correct but represented different units.&lt;/p&gt;

&lt;p&gt;One system stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 CASE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12 EACH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A third had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without enough context.&lt;/p&gt;

&lt;p&gt;The reconciliation needed both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Quantity
+
Unit of measure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before comparison.&lt;/p&gt;

&lt;p&gt;So we normalized quantities into the agreed base unit.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toBaseUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;conversionFactor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;conversionFactor&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;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 CASES × 12 EACH
=
24 EACH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only normalized quantities were used in reconciliation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fourth Variance: In-Flight Transactions
&lt;/h2&gt;

&lt;p&gt;Some inventory existed between states.&lt;/p&gt;

&lt;p&gt;Examples included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;warehouse transfers,&lt;/li&gt;
&lt;li&gt;receipts not yet posted,&lt;/li&gt;
&lt;li&gt;shipments picked but not confirmed,&lt;/li&gt;
&lt;li&gt;purchase receipts awaiting interface processing,&lt;/li&gt;
&lt;li&gt;adjustments waiting in an integration queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These records were dangerous because every system could legitimately represent them differently.&lt;/p&gt;

&lt;p&gt;Consider an inter-warehouse transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warehouse A
-100 units

In Transit
+100 units

Warehouse B
0 units until receipt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the new ERP loaded only warehouse balances and ignored in-transit stock, those 100 units effectively disappeared.&lt;/p&gt;

&lt;p&gt;We therefore created a separate reconciliation category for &lt;strong&gt;in-flight inventory&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It could not hide inside ordinary on-hand quantities.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Built a Canonical Inventory Snapshot
&lt;/h2&gt;

&lt;p&gt;Comparing System A directly with System B and then System B with System C became difficult.&lt;/p&gt;

&lt;p&gt;Each system used different field names and definitions.&lt;/p&gt;

&lt;p&gt;Instead, we transformed every extract into the same structure.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warehouse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WH-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AVAILABLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"snapshotTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cutover"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the process became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Legacy ERP ──┐
             │
WMS ─────────┼──→ Canonical Format
             │          ↓
New ERP ─────┘      Reconciliation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That made comparison much simpler.&lt;/p&gt;

&lt;p&gt;The reconciliation engine did not need to understand three ERP/WMS schemas.&lt;/p&gt;

&lt;p&gt;It only needed to understand one canonical inventory model.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reconciliation Was Automated
&lt;/h2&gt;

&lt;p&gt;Forty thousand inventory combinations cannot be reliably compared in spreadsheets during a stressful cutover weekend.&lt;/p&gt;

&lt;p&gt;We generated a reconciliation dataset.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;legacy_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wms_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;new_erp_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;legacy_qty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;wms_qty&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;legacy_wms_variance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wms_qty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;new_erp_qty&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;wms_new_variance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inventory_reconciliation&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;legacy_qty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;wms_qty&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;wms_qty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;new_erp_qty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looked more like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SKU&lt;/th&gt;
&lt;th&gt;Warehouse&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Legacy&lt;/th&gt;
&lt;th&gt;WMS&lt;/th&gt;
&lt;th&gt;New ERP&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A100&lt;/td&gt;
&lt;td&gt;WH01&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B210&lt;/td&gt;
&lt;td&gt;WH01&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;td&gt;INVESTIGATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C330&lt;/td&gt;
&lt;td&gt;WH02&lt;/td&gt;
&lt;td&gt;Hold&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D115&lt;/td&gt;
&lt;td&gt;WH03&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the team reviewed exceptions rather than manually checking every record.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every Variance Needed a Reason
&lt;/h2&gt;

&lt;p&gt;We did not immediately “fix” every mismatch.&lt;/p&gt;

&lt;p&gt;First, we classified it.&lt;/p&gt;

&lt;p&gt;Our variance categories became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TIMING
UOM_MAPPING
STATUS_MAPPING
SKU_MAPPING
IN_FLIGHT_TRANSACTION
MISSING_TRANSACTION
DUPLICATE_LOAD
MANUAL_ADJUSTMENT
UNKNOWN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was extremely useful.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKU: A-1044
Variance: +24
Reason: UOM_MAPPING
Action: Correct case-to-each conversion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKU: B-2291
Variance: -3
Reason: TIMING
Action: Include receipt posted before cutoff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A number without a reason is just another number.&lt;/p&gt;

&lt;p&gt;A classified variance tells you what to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Added Control Totals
&lt;/h2&gt;

&lt;p&gt;Detailed reconciliation caught SKU-level problems.&lt;/p&gt;

&lt;p&gt;Control totals told us whether the entire migration was structurally sound.&lt;/p&gt;

&lt;p&gt;We checked totals such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total SKU/location combinations
Total physical quantity
Total available quantity
Total blocked quantity
Total inventory value
Total serialized units
Total lot-controlled quantities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected inventory value:  $12,482,911.40
New ERP inventory value:   $12,482,911.40
Variance:                  $0.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But we never treated a matching inventory value as proof that quantities were correct.&lt;/p&gt;

&lt;p&gt;Two errors can cancel each other out.&lt;/p&gt;

&lt;p&gt;Detailed reconciliation and control totals served different purposes.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Rehearsed the Cutover
&lt;/h2&gt;

&lt;p&gt;The final reconciliation logic was not created during go-live.&lt;/p&gt;

&lt;p&gt;We ran mock cutovers first.&lt;/p&gt;

&lt;p&gt;Each rehearsal tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;extraction duration,&lt;/li&gt;
&lt;li&gt;load duration,&lt;/li&gt;
&lt;li&gt;transaction freeze,&lt;/li&gt;
&lt;li&gt;integration shutdown,&lt;/li&gt;
&lt;li&gt;inventory mapping,&lt;/li&gt;
&lt;li&gt;reconciliation scripts,&lt;/li&gt;
&lt;li&gt;exception handling,&lt;/li&gt;
&lt;li&gt;business sign-off,&lt;/li&gt;
&lt;li&gt;rollback timing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every rehearsal revealed something.&lt;/p&gt;

&lt;p&gt;One exposed an unmapped warehouse.&lt;/p&gt;

&lt;p&gt;Another found duplicate SKU aliases.&lt;/p&gt;

&lt;p&gt;Another showed that an integration queue could still post transactions after the supposed freeze.&lt;/p&gt;

&lt;p&gt;That was exactly why we rehearsed.&lt;/p&gt;

&lt;p&gt;The purpose of a mock cutover is not to prove the plan is good.&lt;/p&gt;

&lt;p&gt;It is to discover where the plan is wrong while failure is still cheap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Our Go/No-Go Rule Became Explicit
&lt;/h2&gt;

&lt;p&gt;One of the worst things a cutover team can hear at 2 AM is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The numbers look close enough.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We defined acceptance rules before the final weekend.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Critical inventory variance:
Must equal zero.

Unknown SKU mapping:
Must equal zero.

Unknown inventory status:
Must equal zero.

Unprocessed integration messages:
Must equal zero.

Inventory valuation:
Must reconcile to approved control total.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some organizations may define tolerances for particular non-critical controls.&lt;/p&gt;

&lt;p&gt;The important part is deciding them &lt;strong&gt;before&lt;/strong&gt; the cutover.&lt;/p&gt;

&lt;p&gt;The person under schedule pressure should not also be inventing the acceptance criteria.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final ERP Cutover Flow
&lt;/h2&gt;

&lt;p&gt;The final runbook looked approximately like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Stop business transactions
             ↓
2. Drain integration queues
             ↓
3. Freeze legacy systems
             ↓
4. Capture final inventory snapshots
             ↓
5. Normalize all three datasets
             ↓
6. Run source reconciliation
             ↓
7. Investigate exceptions
             ↓
8. Load opening inventory
             ↓
9. Reconcile new ERP
             ↓
10. Business sign-off
             ↓
11. Go / No-Go decision
             ↓
12. Resume operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration script was only one step.&lt;/p&gt;

&lt;p&gt;The reconciliation process was what made the cutover trustworthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Went Wrong
&lt;/h2&gt;

&lt;p&gt;Looking back, our biggest mistakes were simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Compared Totals Too Early
&lt;/h3&gt;

&lt;p&gt;A matching grand total hid location- and status-level errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Did Not Define the Cutoff Precisely Enough
&lt;/h3&gt;

&lt;p&gt;Systems representing different moments cannot be reconciled meaningfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Treated Inventory Status as a Label
&lt;/h3&gt;

&lt;p&gt;It was actually business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Underestimated In-Flight Transactions
&lt;/h3&gt;

&lt;p&gt;Inventory between locations or process states needed explicit treatment.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Relied Too Much on Spreadsheets
&lt;/h3&gt;

&lt;p&gt;Spreadsheets were useful for investigation, but automated reconciliation was safer for repeatable cutover validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Started With Data Movement Instead of Reconciliation Design
&lt;/h3&gt;

&lt;p&gt;Knowing how data would be proved correct should have come before writing the final migration process.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Worked
&lt;/h2&gt;

&lt;p&gt;The strongest decisions were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define one cutoff timestamp,&lt;/li&gt;
&lt;li&gt;freeze transactions before final reconciliation,&lt;/li&gt;
&lt;li&gt;normalize every system into one inventory model,&lt;/li&gt;
&lt;li&gt;compare at SKU/location/status level,&lt;/li&gt;
&lt;li&gt;normalize units of measure,&lt;/li&gt;
&lt;li&gt;isolate in-flight inventory,&lt;/li&gt;
&lt;li&gt;automate variance detection,&lt;/li&gt;
&lt;li&gt;classify every exception,&lt;/li&gt;
&lt;li&gt;establish control totals,&lt;/li&gt;
&lt;li&gt;rehearse the full cutover,&lt;/li&gt;
&lt;li&gt;define go/no-go rules before go-live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Legacy ERP ──┐
             │
WMS ─────────┼──→ Normalize
             │       ↓
New ERP ─────┘   Reconcile
                     ↓
                 Exceptions
                     ↓
                  Sign-Off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple architecture.&lt;/p&gt;

&lt;p&gt;Strict controls.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The hardest part of inventory migration was not copying inventory into the new ERP.&lt;/p&gt;

&lt;p&gt;It was agreeing on what &lt;strong&gt;inventory&lt;/strong&gt; meant.&lt;/p&gt;

&lt;p&gt;Was stock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On hand?
Available?
Allocated?
In transit?
On hold?
Damaged?
Received but not posted?
Picked but not shipped?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three systems could all be correct according to their own definitions and still disagree with each other.&lt;/p&gt;

&lt;p&gt;That is why reconciliation has to start with business meaning, not database columns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;A safe &lt;strong&gt;ERP cutover&lt;/strong&gt; should not ask only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did the inventory load succeed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did we capture the same point in time?

Do SKU and location quantities agree?

Do statuses mean the same thing?

Were units normalized?

Are in-flight transactions accounted for?

Does inventory value reconcile?

Can every remaining variance be explained?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those questions cannot be answered, the inventory is not reconciled.&lt;/p&gt;

&lt;p&gt;The most important artifact from our cutover was not the migration script.&lt;/p&gt;

&lt;p&gt;It was the evidence that allowed the business to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;These opening balances are trustworthy.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is what reconciliation is really for.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Migrating 40,000 Pages From WordPress to Astro</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Tue, 01 Sep 2026 10:09:37 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/migrating-40000-pages-from-wordpress-to-astro-4ibg</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/migrating-40000-pages-from-wordpress-to-astro-4ibg</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0quh91l3g5sdg99a7aqu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0quh91l3g5sdg99a7aqu.png" alt="Migrating 40,000 Pages From WordPress to Astro" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Migrating a small WordPress site to Astro is mostly a development task.&lt;/p&gt;

&lt;p&gt;Migrating &lt;strong&gt;40,000 pages&lt;/strong&gt; is something else entirely.&lt;/p&gt;

&lt;p&gt;It becomes a content, URL, SEO, media, routing, and deployment migration—all at once.&lt;/p&gt;

&lt;p&gt;Our first plan looked simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Export WordPress
      ↓
Convert content
      ↓
Build with Astro
      ↓
Deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real process looked more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inventory
   ↓
Extract
   ↓
Normalize
   ↓
Transform
   ↓
Generate
   ↓
Validate
   ↓
Deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Astro components were not the hardest part.&lt;/p&gt;

&lt;p&gt;Preserving years of WordPress history was.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a representative large-scale migration post-mortem, not a customer case study.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The First Mistake: Building Before Inventorying
&lt;/h2&gt;

&lt;p&gt;We started by creating Astro components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Header.astro
ArticleLayout.astro
Footer.astro
RelatedPosts.astro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first migrated pages looked perfect.&lt;/p&gt;

&lt;p&gt;Then we reached older content.&lt;/p&gt;

&lt;p&gt;We found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;legacy shortcodes,&lt;/li&gt;
&lt;li&gt;page-builder markup,&lt;/li&gt;
&lt;li&gt;inline CSS,&lt;/li&gt;
&lt;li&gt;broken HTML,&lt;/li&gt;
&lt;li&gt;old CDN URLs,&lt;/li&gt;
&lt;li&gt;embedded forms,&lt;/li&gt;
&lt;li&gt;custom blocks,&lt;/li&gt;
&lt;li&gt;absolute internal links,&lt;/li&gt;
&lt;li&gt;obsolete plugin output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changed the question.&lt;/p&gt;

&lt;p&gt;It was no longer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can Astro render WordPress content?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What has 15 years of WordPress actually stored?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Start With Every Existing URL
&lt;/h2&gt;

&lt;p&gt;The most useful artifact in the entire migration became a URL manifest.&lt;/p&gt;

&lt;p&gt;For every public URL, we recorded something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/guides/performance/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/guides/performance/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"page"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave us a clear contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old URL exists
      ↓
New URL exists
      ↓
Same path OR redirect exists
      ↓
Correct status
      ↓
Correct canonical
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this inventory, there was no reliable way to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did all 40,000 pages actually migrate?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Treat WordPress Export Like an ETL Pipeline
&lt;/h2&gt;

&lt;p&gt;Forty thousand records are not something you should treat as one giant API response.&lt;/p&gt;

&lt;p&gt;WordPress REST API collections are paginated, so extraction needs to happen in batches.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&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="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/wp-json/wp/v2/pages?per_page=100&amp;amp;page=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;page&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important change was adding checkpoints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fetch
 ↓
Normalize
 ↓
Save
 ↓
Checkpoint
 ↓
Continue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If batch 287 failed, we restarted from batch 287.&lt;/p&gt;

&lt;p&gt;Not from zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do Not Couple Astro Directly to WordPress Data
&lt;/h2&gt;

&lt;p&gt;Instead of passing raw WordPress API responses through the application, we created a normalized content model.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MigratedPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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;p&gt;That gave us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress
   ↓
Extraction
   ↓
Normalized Content
   ↓
Astro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now WordPress-specific decisions stayed in the migration layer instead of leaking into every Astro component.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not Everything Should Become Markdown
&lt;/h2&gt;

&lt;p&gt;We originally considered converting all content to Markdown.&lt;/p&gt;

&lt;p&gt;That worked for clean pages.&lt;/p&gt;

&lt;p&gt;It failed on years of legacy content.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[pricing_table product="enterprise"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"legacy-callout"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A blind HTML-to-Markdown conversion could silently remove functionality or structure.&lt;/p&gt;

&lt;p&gt;So we used three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;convert clean content,&lt;/li&gt;
&lt;li&gt;transform known legacy patterns,&lt;/li&gt;
&lt;li&gt;preserve sanitized HTML when conversion would lose information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was not format purity.&lt;/p&gt;

&lt;p&gt;The goal was &lt;strong&gt;content fidelity&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shortcodes Need Explicit Replacement Rules
&lt;/h2&gt;

&lt;p&gt;WordPress understood old shortcodes because plugins interpreted them.&lt;/p&gt;

&lt;p&gt;Astro does not.&lt;/p&gt;

&lt;p&gt;We therefore classified them as:&lt;/p&gt;

&lt;h3&gt;
  
  
  Static
&lt;/h3&gt;

&lt;p&gt;Convert during migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic
&lt;/h3&gt;

&lt;p&gt;Replace with a real Astro component or service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dead
&lt;/h3&gt;

&lt;p&gt;Remove or rewrite intentionally.&lt;/p&gt;

&lt;p&gt;Unknown shortcodes became migration failures.&lt;/p&gt;

&lt;p&gt;That was better than discovering this in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[old_widget id="32"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Preserve URLs Unless You Have a Good Reason Not To
&lt;/h2&gt;

&lt;p&gt;A framework migration is already risky.&lt;/p&gt;

&lt;p&gt;We did not want to combine it with a complete URL redesign.&lt;/p&gt;

&lt;p&gt;If WordPress used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/guides/astro-migration/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astro normally kept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/guides/astro-migration/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a URL genuinely changed, the redirect was stored as migration data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/old-guide/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/guides/new-guide/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also standardized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trailing slashes,&lt;/li&gt;
&lt;li&gt;canonical URLs,&lt;/li&gt;
&lt;li&gt;internal links,&lt;/li&gt;
&lt;li&gt;sitemap URLs,&lt;/li&gt;
&lt;li&gt;redirect destinations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Routing was not merely a frontend decision.&lt;/p&gt;

&lt;p&gt;It was an SEO requirement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Media Was More Than &lt;code&gt;/uploads&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Images existed in more places than expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;page HTML,&lt;/li&gt;
&lt;li&gt;featured images,&lt;/li&gt;
&lt;li&gt;metadata,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;srcset&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;custom fields,&lt;/li&gt;
&lt;li&gt;CSS,&lt;/li&gt;
&lt;li&gt;old CDN URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So assets received their own manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://old.example.com/uploads/hero.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/media/hero.jpg"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allowed us to detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing files,&lt;/li&gt;
&lt;li&gt;old WordPress URLs,&lt;/li&gt;
&lt;li&gt;failed rewrites,&lt;/li&gt;
&lt;li&gt;broken image references.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 40,000 pages, guessing whether every asset migrated was not acceptable.&lt;/p&gt;




&lt;h2&gt;
  
  
  40,000 Static Pages Change Build Economics
&lt;/h2&gt;

&lt;p&gt;Astro can generate large numbers of static routes.&lt;/p&gt;

&lt;p&gt;But static pages are not free to build.&lt;/p&gt;

&lt;p&gt;Every generated page requires work.&lt;/p&gt;

&lt;p&gt;At this size, scalable &lt;a href="https://sdlccorp.com/web-development-company/" rel="noopener noreferrer"&gt;web development&lt;/a&gt; also means thinking about build times, routing strategy, caching, and deployment economics—not only frontend rendering.&lt;/p&gt;

&lt;p&gt;So we had to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does every page need to be regenerated on every deployment?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer depends on the site.&lt;/p&gt;

&lt;p&gt;Possible approaches include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fully static
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Static important routes
+
on-demand rendering where appropriate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key lesson was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fast runtime performance does not automatically mean fast builds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Build time became an architecture constraint.&lt;/p&gt;




&lt;h2&gt;
  
  
  SEO Metadata Is Content Too
&lt;/h2&gt;

&lt;p&gt;Moving the visible article was not enough.&lt;/p&gt;

&lt;p&gt;We also needed to preserve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;titles,&lt;/li&gt;
&lt;li&gt;meta descriptions,&lt;/li&gt;
&lt;li&gt;canonical URLs,&lt;/li&gt;
&lt;li&gt;publication dates,&lt;/li&gt;
&lt;li&gt;authors,&lt;/li&gt;
&lt;li&gt;structured data,&lt;/li&gt;
&lt;li&gt;social metadata where needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A page could render perfectly and still become an SEO migration failure.&lt;/p&gt;

&lt;p&gt;That made metadata part of our normalized content model rather than an afterthought.&lt;/p&gt;

&lt;p&gt;A large migration needs an &lt;a href="https://sdlccorp.com/website-development-company/" rel="noopener noreferrer"&gt;SEO-first web architecture&lt;/a&gt; so clean URLs, metadata, structured data, redirects, and crawlability survive the platform change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validation Became More Important Than Conversion
&lt;/h2&gt;

&lt;p&gt;Eventually, most of our confidence came from automated validation.&lt;/p&gt;

&lt;p&gt;For every expected URL, we checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Route exists?
      ↓
Correct status?
      ↓
Title present?
      ↓
Canonical correct?
      ↓
Content present?
      ↓
Images valid?
      ↓
Internal links valid?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also made certain errors fail the build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing expected routes,&lt;/li&gt;
&lt;li&gt;duplicate slugs,&lt;/li&gt;
&lt;li&gt;unknown shortcodes,&lt;/li&gt;
&lt;li&gt;broken internal links,&lt;/li&gt;
&lt;li&gt;missing canonicals,&lt;/li&gt;
&lt;li&gt;redirect loops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our rule became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a machine can prove the migration is broken, a human should not need to discover it after launch.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Cutover Needed a Delta Migration
&lt;/h2&gt;

&lt;p&gt;A large migration does not happen instantly.&lt;/p&gt;

&lt;p&gt;Editors may continue publishing while the migration runs.&lt;/p&gt;

&lt;p&gt;That means the original export quickly becomes outdated.&lt;/p&gt;

&lt;p&gt;Our cutover flow looked roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Full migration
      ↓
Validate
      ↓
Sync recent changes
      ↓
Short publishing freeze
      ↓
Final delta sync
      ↓
Deploy
      ↓
Production validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also kept WordPress available during the rollback window.&lt;/p&gt;

&lt;p&gt;A migration is safer when the previous production system remains recoverable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Went Wrong
&lt;/h2&gt;

&lt;p&gt;The biggest mistakes were straightforward.&lt;/p&gt;

&lt;h3&gt;
  
  
  We started with components instead of inventory
&lt;/h3&gt;

&lt;p&gt;Building UI felt productive, but understanding the existing site was more important.&lt;/p&gt;

&lt;h3&gt;
  
  
  We underestimated legacy content
&lt;/h3&gt;

&lt;p&gt;Forty thousand pages created over many years do not follow one clean format.&lt;/p&gt;

&lt;h3&gt;
  
  
  We assumed Markdown should be the destination
&lt;/h3&gt;

&lt;p&gt;Sometimes preserving HTML was safer.&lt;/p&gt;

&lt;h3&gt;
  
  
  We treated redirects as configuration
&lt;/h3&gt;

&lt;p&gt;They were actually migration data.&lt;/p&gt;

&lt;h3&gt;
  
  
  We assumed static generation solved every performance problem
&lt;/h3&gt;

&lt;p&gt;Runtime performance and build performance are different concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  We manually checked pages instead of validating the system
&lt;/h3&gt;

&lt;p&gt;Automation gave us much more confidence than random visual QA.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Worked
&lt;/h2&gt;

&lt;p&gt;The strongest decisions were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inventory every public URL,&lt;/li&gt;
&lt;li&gt;normalize WordPress data before Astro sees it,&lt;/li&gt;
&lt;li&gt;migrate page types instead of individual pages,&lt;/li&gt;
&lt;li&gt;preserve URLs whenever possible,&lt;/li&gt;
&lt;li&gt;track assets separately,&lt;/li&gt;
&lt;li&gt;validate automatically,&lt;/li&gt;
&lt;li&gt;fail on unknown legacy content,&lt;/li&gt;
&lt;li&gt;plan the final content sync early.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final architecture became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress
    ↓
Extract
    ↓
Normalize
    ↓
 ┌─────────────┐
 │   Content   │
 │   Assets    │
 │  Redirects  │
 └─────────────┘
       ↓
      Astro
       ↓
   Validation
       ↓
    Production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The hardest part of a &lt;strong&gt;WordPress to Astro migration&lt;/strong&gt; was not Astro.&lt;/p&gt;

&lt;p&gt;It was WordPress history.&lt;/p&gt;

&lt;p&gt;The old system represented much more than PHP and a database.&lt;/p&gt;

&lt;p&gt;It contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;40,000 URLs
years of content
SEO history
redirects
media
plugins
shortcodes
custom markup
undocumented assumptions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astro did not need to reproduce WordPress.&lt;/p&gt;

&lt;p&gt;It needed to preserve the parts users and search engines depended on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;For a large &lt;strong&gt;WordPress to Astro migration&lt;/strong&gt;, do not start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create astro@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with three questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What exists today?

What must survive?

How will we prove it survived?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build the migration around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inventory
   ↓
Extract
   ↓
Normalize
   ↓
Transform
   ↓
Generate
   ↓
Validate
   ↓
Deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework was the new delivery layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real engineering work was migrating 40,000 content contracts without breaking them.&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>We Cut LCP From 6s to 1.2s Every Change, Measured</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Tue, 01 Sep 2026 05:24:43 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/we-cut-lcp-from-6s-to-12s-every-change-measured-175c</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/we-cut-lcp-from-6s-to-12s-every-change-measured-175c</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7mcqlo4sk66vybdkvfa7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7mcqlo4sk66vybdkvfa7.png" alt="We Cut LCP From 6s to 1.2s Every Change, Measured" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
The page did not look broken.&lt;/p&gt;

&lt;p&gt;It loaded.&lt;/p&gt;

&lt;p&gt;The hero appeared.&lt;/p&gt;

&lt;p&gt;The buttons worked.&lt;/p&gt;

&lt;p&gt;Nothing obvious screamed &lt;strong&gt;“performance problem.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But under mobile conditions, Largest Contentful Paint was taking roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6.0 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was not a small regression.&lt;/p&gt;

&lt;p&gt;It was more than twice the &lt;strong&gt;2.5-second threshold&lt;/strong&gt; generally considered good for LCP.&lt;/p&gt;

&lt;p&gt;So instead of throwing every common performance trick at the page, we treated the problem like a debugging exercise.&lt;/p&gt;

&lt;p&gt;One change.&lt;/p&gt;

&lt;p&gt;One measurement.&lt;/p&gt;

&lt;p&gt;Then the next change.&lt;/p&gt;

&lt;p&gt;The final result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: 6.0s LCP
After:  1.2s LCP
Change: -80%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part was not the final number.&lt;/p&gt;

&lt;p&gt;It was discovering that the largest image was &lt;strong&gt;not initially the biggest problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how the &lt;strong&gt;LCP optimization&lt;/strong&gt; unfolded.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a representative performance case study. The measurements are designed to illustrate the debugging and optimization process rather than represent a specific customer project.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  First, We Made the Test Repeatable
&lt;/h2&gt;

&lt;p&gt;Performance work becomes unreliable when every change is measured under different conditions.&lt;/p&gt;

&lt;p&gt;Before touching the application, we created a repeatable baseline.&lt;/p&gt;

&lt;p&gt;The same principle applies to &lt;a href="https://sdlccorp.com/services/testing-services/" rel="noopener noreferrer"&gt;performance and load testing&lt;/a&gt;: if the environment and conditions keep changing, the results are difficult to compare or trust.&lt;/p&gt;

&lt;p&gt;For each version, we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tested the same URL&lt;/li&gt;
&lt;li&gt;Used the same mobile profile&lt;/li&gt;
&lt;li&gt;Started with a cold cache&lt;/li&gt;
&lt;li&gt;Used the same network conditions&lt;/li&gt;
&lt;li&gt;Ran multiple tests&lt;/li&gt;
&lt;li&gt;Ignored obvious outliers&lt;/li&gt;
&lt;li&gt;Compared the median result&lt;/li&gt;
&lt;li&gt;Recorded the LCP element and timing breakdown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lab tools helped us iterate quickly.&lt;/p&gt;

&lt;p&gt;Real-user monitoring was still important for validating the final result because actual users have different devices, networks, locations, and cache states.&lt;/p&gt;

&lt;p&gt;The lab was our debugging environment.&lt;/p&gt;

&lt;p&gt;Field data was our reality check.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Baseline: Where Were the Six Seconds Going?
&lt;/h2&gt;

&lt;p&gt;Looking at a single LCP number was not enough.&lt;/p&gt;

&lt;p&gt;We broke it into four parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;LCP Component&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to First Byte&lt;/td&gt;
&lt;td&gt;1.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource load delay&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource load duration&lt;/td&gt;
&lt;td&gt;1.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Element render delay&lt;/td&gt;
&lt;td&gt;1.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total LCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.0s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table immediately changed our approach.&lt;/p&gt;

&lt;p&gt;The hero image took about one second to download.&lt;/p&gt;

&lt;p&gt;But the browser spent &lt;strong&gt;2.1 seconds waiting before it even started downloading it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Compressing the image first would have attacked the wrong bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change #1: Fix the 1.5s TTFB
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LCP: 6.0s → 5.0s
&lt;/h3&gt;

&lt;p&gt;The document itself was arriving too slowly.&lt;/p&gt;

&lt;p&gt;Our initial request involved application work that did not need to happen for every page view.&lt;/p&gt;

&lt;p&gt;Conceptually, the request looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   ↓
Application
   ↓
Database/API work
   ↓
Template rendering
   ↓
HTML response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Until that HTML starts arriving, the browser cannot discover most of the resources needed to render the page.&lt;/p&gt;

&lt;p&gt;We reduced that delay by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Caching cacheable HTML responses&lt;/li&gt;
&lt;li&gt;Removing unnecessary server-side work&lt;/li&gt;
&lt;li&gt;Avoiding repeated data fetches&lt;/li&gt;
&lt;li&gt;Moving non-critical work out of the request path&lt;/li&gt;
&lt;li&gt;Serving cached responses closer to users where appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The TTFB moved from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.5s → 0.5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our new breakdown looked approximately like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;After Change #1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TTFB&lt;/td&gt;
&lt;td&gt;0.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource load delay&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource load duration&lt;/td&gt;
&lt;td&gt;1.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render delay&lt;/td&gt;
&lt;td&gt;1.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.0s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A one-second improvement was useful.&lt;/p&gt;

&lt;p&gt;But five seconds was still terrible.&lt;/p&gt;

&lt;p&gt;More importantly, the trace now made the next problem impossible to ignore.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change #2: Let the Browser Discover the Hero Immediately
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LCP: 5.0s → 3.1s
&lt;/h3&gt;

&lt;p&gt;This was the biggest single improvement.&lt;/p&gt;

&lt;p&gt;Our LCP element was the hero image.&lt;/p&gt;

&lt;p&gt;But the browser could not discover it early.&lt;/p&gt;

&lt;p&gt;The implementation effectively depended on other resources being processed before the hero request became visible.&lt;/p&gt;

&lt;p&gt;Patterns like this commonly cause trouble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("/images/hero.webp")&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;p&gt;If that CSS is in an external stylesheet, the browser needs to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive HTML
   ↓
Discover CSS
   ↓
Download CSS
   ↓
Parse CSS
   ↓
Discover hero image
   ↓
Request hero image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image may be highly compressed.&lt;/p&gt;

&lt;p&gt;That does not matter if its request starts two seconds late.&lt;/p&gt;

&lt;p&gt;We moved the important visual into the document as an actual image element.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
    &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero-1280.avif"&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero-1280.webp"&lt;/span&gt;
    &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1280"&lt;/span&gt;
    &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"720"&lt;/span&gt;
    &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Application dashboard"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also verified something important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;loading="lazy"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;was &lt;strong&gt;not&lt;/strong&gt; applied to the LCP image.&lt;/p&gt;

&lt;p&gt;Lazy loading below-the-fold images is useful.&lt;/p&gt;

&lt;p&gt;Lazy loading the element that is supposed to become your largest above-the-fold paint is usually counterproductive.&lt;/p&gt;

&lt;p&gt;After the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resource load delay:

2.1s → 0.2s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And overall LCP dropped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5.0s → 3.1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single change saved almost two seconds.&lt;/p&gt;

&lt;p&gt;This was the most important lesson from the entire investigation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not only ask how fast the LCP resource downloads. Ask how quickly the browser discovers it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Change #3: Now We Optimized the Image
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LCP: 3.1s → 2.4s
&lt;/h3&gt;

&lt;p&gt;Only now did image optimization become the largest obvious opportunity.&lt;/p&gt;

&lt;p&gt;The original hero asset was much larger than necessary.&lt;/p&gt;

&lt;p&gt;We addressed three things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Format
&lt;/h3&gt;

&lt;p&gt;We generated modern image variants instead of sending the same large source everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimensions
&lt;/h3&gt;

&lt;p&gt;A mobile device did not need an image sized for a large desktop display.&lt;/p&gt;

&lt;p&gt;Responsive image candidates allowed the browser to choose an appropriate resource.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero-1280.webp"&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"
    /images/hero-640.webp 640w,
    /images/hero-960.webp 960w,
    /images/hero-1280.webp 1280w
  "&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1280"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"720"&lt;/span&gt;
  &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Application dashboard"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compression
&lt;/h3&gt;

&lt;p&gt;We reduced bytes without making the hero visibly degraded.&lt;/p&gt;

&lt;p&gt;The result was a substantial reduction in resource transfer time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resource load duration:

1.0s → 0.3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Overall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3.1s → 2.4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We had finally crossed the 2.5-second line in our controlled test.&lt;/p&gt;

&lt;p&gt;But stopping here would have meant leaving almost another second on the table.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change #4: Remove Work Blocking the Paint
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LCP: 2.4s → 1.5s
&lt;/h3&gt;

&lt;p&gt;At this point the hero was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discovered early&lt;/li&gt;
&lt;li&gt;Prioritized&lt;/li&gt;
&lt;li&gt;Relatively small&lt;/li&gt;
&lt;li&gt;Downloaded quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yet there was still a noticeable gap between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Image downloaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Image painted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is element render delay.&lt;/p&gt;

&lt;p&gt;The culprit was not the image anymore.&lt;/p&gt;

&lt;p&gt;The browser's critical rendering path contained resources that delayed the initial render.&lt;/p&gt;

&lt;p&gt;We found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS that was not required above the fold&lt;/li&gt;
&lt;li&gt;Scripts executing before they were needed&lt;/li&gt;
&lt;li&gt;Third-party code competing for the main thread&lt;/li&gt;
&lt;li&gt;Application code doing work before initial content could settle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We started removing things from the critical path.&lt;/p&gt;

&lt;p&gt;Non-critical JavaScript was deferred where possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/analytics.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Non-essential functionality stopped competing with the first render.&lt;/p&gt;

&lt;p&gt;We also reviewed the CSS loaded before the hero appeared.&lt;/p&gt;

&lt;p&gt;This kind of &lt;a href="https://sdlccorp.com/web-development-company/" rel="noopener noreferrer"&gt;web performance optimization&lt;/a&gt; is easier when frontend architecture, asset loading, and rendering behavior are treated as part of the same performance problem.&lt;/p&gt;

&lt;p&gt;The rule was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If the resource is not necessary to display the first screen, why is the first screen waiting for it?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After that cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Element render delay:

1.4s → 0.5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2.4s → 1.5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page now felt dramatically different even though visually almost nothing had changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change #5: Reduce Main-Thread Work Before LCP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LCP: 1.5s → 1.2s
&lt;/h3&gt;

&lt;p&gt;The final improvement was smaller, but it exposed another useful performance pattern.&lt;/p&gt;

&lt;p&gt;The main thread was still doing too much work early.&lt;/p&gt;

&lt;p&gt;Large JavaScript bundles were being:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;downloaded
    ↓
parsed
    ↓
compiled
    ↓
executed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before some of that functionality was actually needed.&lt;/p&gt;

&lt;p&gt;We inspected long tasks occurring before LCP and asked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this code need to execute immediately?&lt;/li&gt;
&lt;li&gt;Can this component initialize after the initial render?&lt;/li&gt;
&lt;li&gt;Can this bundle be split?&lt;/li&gt;
&lt;li&gt;Is a third-party script blocking useful work?&lt;/li&gt;
&lt;li&gt;Are we hydrating components that are below the fold?&lt;/li&gt;
&lt;li&gt;Are we shipping JavaScript for features that are not used on this route?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We delayed non-critical initialization and reduced early JavaScript execution.&lt;/p&gt;

&lt;p&gt;The remaining render delay dropped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.5s → 0.2s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That brought the final measurement to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.2s LCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Every Change, Side by Side
&lt;/h2&gt;

&lt;p&gt;Here is the complete progression:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;LCP&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;6.0s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faster TTFB&lt;/td&gt;
&lt;td&gt;5.0s&lt;/td&gt;
&lt;td&gt;-1.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earlier LCP discovery&lt;/td&gt;
&lt;td&gt;3.1s&lt;/td&gt;
&lt;td&gt;-1.9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Responsive optimized image&lt;/td&gt;
&lt;td&gt;2.4s&lt;/td&gt;
&lt;td&gt;-0.7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove render-blocking work&lt;/td&gt;
&lt;td&gt;1.5s&lt;/td&gt;
&lt;td&gt;-0.9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reduce early main-thread work&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.2s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-0.3s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Overall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6.0 seconds
     ↓
1.2 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;strong&gt;80% reduction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But the table also shows why random optimization is inefficient.&lt;/p&gt;

&lt;p&gt;Our largest improvement did &lt;strong&gt;not&lt;/strong&gt; come from compressing the image.&lt;/p&gt;

&lt;p&gt;It came from starting its request earlier.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final LCP Breakdown
&lt;/h2&gt;

&lt;p&gt;The baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TTFB                  1.5s
Resource load delay   2.1s
Resource load time    1.0s
Render delay          1.4s
                     -----
LCP                    6.0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After optimization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TTFB                  0.5s
Resource load delay   0.2s
Resource load time    0.3s
Render delay          0.2s
                     -----
LCP                    1.2s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That decomposition became much more useful than the Lighthouse score itself.&lt;/p&gt;

&lt;p&gt;A score tells you something is wrong.&lt;/p&gt;

&lt;p&gt;A timing breakdown tells you &lt;strong&gt;where to look&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Did Not Do
&lt;/h2&gt;

&lt;p&gt;Performance work also requires knowing which optimizations &lt;em&gt;not&lt;/em&gt; to make.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Did Not Lazy-Load the Hero
&lt;/h3&gt;

&lt;p&gt;Lazy loading is valuable for resources outside the viewport.&lt;/p&gt;

&lt;p&gt;The LCP image was immediately visible.&lt;/p&gt;

&lt;p&gt;Delaying it made no sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Did Not Preload Everything
&lt;/h3&gt;

&lt;p&gt;Preload is not a general-purpose &lt;strong&gt;“make the site faster”&lt;/strong&gt; switch.&lt;/p&gt;

&lt;p&gt;Making every resource high priority means nothing is actually high priority.&lt;/p&gt;

&lt;p&gt;We reserved resource priority for things that genuinely affected the initial experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Did Not Optimize Only for Lighthouse
&lt;/h3&gt;

&lt;p&gt;Lab measurements helped us compare changes quickly.&lt;/p&gt;

&lt;p&gt;But real users do not all have the same CPU, connection, viewport, cache state, or geographic location.&lt;/p&gt;

&lt;p&gt;Field performance still mattered.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Did Not Start by Removing Random JavaScript
&lt;/h3&gt;

&lt;p&gt;“JavaScript is slow” was not a useful diagnosis.&lt;/p&gt;

&lt;p&gt;We identified which main-thread work overlapped the LCP window and targeted that work specifically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Our LCP Optimization Checklist Changed After This
&lt;/h2&gt;

&lt;p&gt;Before this investigation, it was tempting to start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compress the hero image.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the order is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Identify the Actual LCP Element
&lt;/h3&gt;

&lt;p&gt;Do not assume it is the hero image.&lt;/p&gt;

&lt;p&gt;Measure it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Break LCP Into Its Components
&lt;/h3&gt;

&lt;p&gt;Look separately at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TTFB&lt;/li&gt;
&lt;li&gt;Resource load delay&lt;/li&gt;
&lt;li&gt;Resource load duration&lt;/li&gt;
&lt;li&gt;Element render delay&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Fix the Largest Delay First
&lt;/h3&gt;

&lt;p&gt;Do not optimize what is easiest.&lt;/p&gt;

&lt;p&gt;Optimize what is expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Make the LCP Resource Discoverable
&lt;/h3&gt;

&lt;p&gt;If it is an image, the browser should ideally discover it directly from the HTML.&lt;/p&gt;

&lt;p&gt;Avoid unnecessary dependency chains.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Give the Resource Appropriate Priority
&lt;/h3&gt;

&lt;p&gt;For an important above-the-fold image, consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;fetchpriority="high"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not lazily load it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Send Fewer Bytes
&lt;/h3&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Appropriately sized images&lt;/li&gt;
&lt;li&gt;Responsive sources&lt;/li&gt;
&lt;li&gt;Modern formats&lt;/li&gt;
&lt;li&gt;Sensible compression&lt;/li&gt;
&lt;li&gt;Efficient delivery&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Look at What Happens After Download
&lt;/h3&gt;

&lt;p&gt;A fully downloaded LCP resource can still sit around waiting to render.&lt;/p&gt;

&lt;p&gt;Inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render-blocking CSS&lt;/li&gt;
&lt;li&gt;Synchronous scripts&lt;/li&gt;
&lt;li&gt;Long tasks&lt;/li&gt;
&lt;li&gt;Client-side rendering&lt;/li&gt;
&lt;li&gt;Hydration&lt;/li&gt;
&lt;li&gt;Third-party JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Validate With Real Users
&lt;/h3&gt;

&lt;p&gt;Your development laptop is not your audience.&lt;/p&gt;

&lt;p&gt;Measure the distribution of actual experiences.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lab Data and Field Data Answer Different Questions
&lt;/h2&gt;

&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;During optimization, we wanted to know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Did this specific change improve the page?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Controlled lab testing is excellent for that.&lt;/p&gt;

&lt;p&gt;But after deployment, the question changes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Are real users actually getting a good experience?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That requires field data.&lt;/p&gt;

&lt;p&gt;A single 1.2-second Lighthouse result does not prove that 75% of production users have a 1.2-second LCP.&lt;/p&gt;

&lt;p&gt;Real users bring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;different phones
different networks
different locations
different cache states
different viewport sizes
different browser conditions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we used lab measurements for iteration and field measurements for validation.&lt;/p&gt;

&lt;p&gt;That prevented us from confusing a fast benchmark with a fast website.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson Was Not “Compress Your Images”
&lt;/h2&gt;

&lt;p&gt;Image optimization mattered.&lt;/p&gt;

&lt;p&gt;Caching mattered.&lt;/p&gt;

&lt;p&gt;JavaScript mattered.&lt;/p&gt;

&lt;p&gt;CSS mattered.&lt;/p&gt;

&lt;p&gt;But none of those were the real lesson.&lt;/p&gt;

&lt;p&gt;The lesson was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;LCP optimization works better when you debug time instead of applying performance tips.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We started with six seconds.&lt;/p&gt;

&lt;p&gt;Then asked where those six seconds were going.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6.0s
├── 1.5s waiting for HTML
├── 2.1s waiting to request the hero
├── 1.0s downloading the hero
└── 1.4s waiting to render it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That immediately gave us a priority order.&lt;/p&gt;

&lt;p&gt;By the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.2s
├── 0.5s TTFB
├── 0.2s resource load delay
├── 0.3s resource load duration
└── 0.2s render delay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We did not make the browser magically faster.&lt;/p&gt;

&lt;p&gt;We simply stopped making it wait.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;If your LCP is six seconds, resist the urge to immediately install another optimization plugin, compress every asset, or rewrite the frontend.&lt;/p&gt;

&lt;p&gt;Measure first.&lt;/p&gt;

&lt;p&gt;Find the LCP element.&lt;/p&gt;

&lt;p&gt;Break the metric into its timing components.&lt;/p&gt;

&lt;p&gt;Then attack the largest delay.&lt;/p&gt;

&lt;p&gt;For us, the sequence was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6.0s
 ↓
5.0s   Faster server response
 ↓
3.1s   Earlier LCP discovery
 ↓
2.4s   Smaller responsive image
 ↓
1.5s   Less render blocking
 ↓
1.2s   Less main-thread work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every change had a reason.&lt;/p&gt;

&lt;p&gt;Every change had a measurement.&lt;/p&gt;

&lt;p&gt;And that is the part of &lt;strong&gt;LCP optimization&lt;/strong&gt; that matters most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not optimize based on assumptions. Optimize based on evidence.&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Debugging Governor Limits in a 4M-Record Org</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:22:20 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/debugging-governor-limits-in-a-4m-record-org-52gh</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/debugging-governor-limits-in-a-4m-record-org-52gh</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6w55czxqz4iiu7kim522.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6w55czxqz4iiu7kim522.png" alt="Debugging Governor Limits in a 4M-Record Org" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Apex governor limits are easy to understand when your Salesforce org has 20,000 records.&lt;/p&gt;

&lt;p&gt;They become much more interesting when one object has more than &lt;strong&gt;4 million&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At that scale, code that looked perfectly reasonable for years can suddenly start failing with errors such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.LimitException: Too many SOQL queries: 101
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.LimitException: Too many query rows: 50001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or the less obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.LimitException: Apex CPU time limit exceeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instinctive response is usually:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We need to bulkify the trigger.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes that is correct.&lt;/p&gt;

&lt;p&gt;But in a large-data-volume org, bulkification is often only the beginning.&lt;/p&gt;

&lt;p&gt;This write-up walks through a representative debugging scenario involving &lt;strong&gt;Apex governor limits&lt;/strong&gt;, roughly four million records, a trigger that had worked for years, and a fix that required changing more than a &lt;code&gt;for&lt;/code&gt; loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The scenario and object names below are representative examples designed to demonstrate the debugging process. They are not a customer post-mortem.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine an organization with a custom object called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usage_Event__c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It stores usage events associated with customer accounts.&lt;/p&gt;

&lt;p&gt;Over several years, the table has grown to approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4,120,000 records
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever an Account changes, an Apex trigger recalculates some usage information.&lt;/p&gt;

&lt;p&gt;The original implementation looked roughly like this:&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="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;events&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;Duration__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;totalDuration&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="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Usage_Event__c&lt;/span&gt; &lt;span class="n"&gt;eventRecord&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;totalDuration&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;eventRecord&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Duration__c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Current_Usage__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totalDuration&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;p&gt;If one Account was updated manually, nothing looked wrong.&lt;/p&gt;

&lt;p&gt;One query.&lt;/p&gt;

&lt;p&gt;A few records.&lt;/p&gt;

&lt;p&gt;Fast execution.&lt;/p&gt;

&lt;p&gt;Then a bulk integration started updating Accounts in groups of 200.&lt;/p&gt;

&lt;p&gt;This is especially important in &lt;a href="https://sdlccorp.com/salesforce-integration-services/" rel="noopener noreferrer"&gt;Salesforce integration architecture&lt;/a&gt;, where external systems can turn what looks like a single-record workflow into sustained bulk transactions.&lt;/p&gt;

&lt;p&gt;That changed everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure #1: Too Many SOQL Queries
&lt;/h2&gt;

&lt;p&gt;Salesforce can execute triggers for collections of records.&lt;/p&gt;

&lt;p&gt;So if 200 Accounts enter this trigger, the query inside the loop may execute 200 times.&lt;/p&gt;

&lt;p&gt;That quickly reaches the synchronous Apex SOQL limit.&lt;/p&gt;

&lt;p&gt;The first failure was predictable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.LimitException: Too many SOQL queries: 101
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The obvious fix was to bulkify the query.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix Attempt #1: Bulkify Everything
&lt;/h2&gt;

&lt;p&gt;We collected the Account IDs first:&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="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;accountIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;accountIds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&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;p&gt;Then queried everything in one operation:&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="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;events&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;Account__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Duration__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="n"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accountIds&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better.&lt;/p&gt;

&lt;p&gt;We had gone from potentially 200 queries to one.&lt;/p&gt;

&lt;p&gt;Problem solved?&lt;/p&gt;

&lt;p&gt;Not quite.&lt;/p&gt;

&lt;p&gt;The next test failed differently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.LimitException: Too many query rows: 50001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the moment the problem changed.&lt;/p&gt;

&lt;p&gt;We were no longer debugging a simple trigger anti-pattern.&lt;/p&gt;

&lt;p&gt;We were debugging &lt;strong&gt;large data volume&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bulkified Does Not Mean Scalable
&lt;/h2&gt;

&lt;p&gt;This is an important distinction.&lt;/p&gt;

&lt;p&gt;This:&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="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SOQL&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is bad because the number of queries grows with the number of trigger records.&lt;/p&gt;

&lt;p&gt;But replacing it with one giant query does not guarantee scalability.&lt;/p&gt;

&lt;p&gt;We had removed one problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Too many SOQL queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and exposed another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Too many queried rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our transaction was now logically bulkified but still trying to bring too much data into one Apex execution context.&lt;/p&gt;

&lt;p&gt;That is where debugging &lt;strong&gt;Apex governor limits&lt;/strong&gt; becomes more architectural.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start With the Transaction, Not the Exception
&lt;/h2&gt;

&lt;p&gt;When a governor limit fails, the last line in the stack trace is not always the real problem.&lt;/p&gt;

&lt;p&gt;Instead, I want to know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What entered the transaction?
        ↓
What automation executed?
        ↓
What queried data?
        ↓
How much data came back?
        ↓
What happened to that data?
        ↓
Which limit grew fastest?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces a much more useful investigation.&lt;/p&gt;

&lt;p&gt;Salesforce transactions may include more than the Apex class you are currently reading.&lt;/p&gt;

&lt;p&gt;They can involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;triggers,&lt;/li&gt;
&lt;li&gt;record-triggered flows,&lt;/li&gt;
&lt;li&gt;validation logic,&lt;/li&gt;
&lt;li&gt;managed-package automation,&lt;/li&gt;
&lt;li&gt;workflow-related actions,&lt;/li&gt;
&lt;li&gt;additional DML,&lt;/li&gt;
&lt;li&gt;additional trigger executions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So “my handler only runs three queries” does not necessarily mean the transaction runs only three queries.&lt;/p&gt;

&lt;p&gt;The governor belongs to the &lt;strong&gt;transaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not your class.&lt;/p&gt;




&lt;h2&gt;
  
  
  Add Limit Instrumentation
&lt;/h2&gt;

&lt;p&gt;Debug logs are useful, but I also like adding temporary instrumentation around suspicious sections.&lt;/p&gt;

&lt;p&gt;For example:&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="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;SOQL: '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLimitQueries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Rows: '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueryRows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLimitQueryRows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;CPU: '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCpuTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLimitCpuTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;DML: '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDmlStatements&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLimitDmlStatements&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;p&gt;Then repeat the measurement around expensive operations.&lt;/p&gt;

&lt;p&gt;For example:&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="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Before usage query CPU: '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCpuTime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;events&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;Account__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Duration__c&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="n"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accountIds&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;After usage query CPU: '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCpuTime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Query rows: '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueryRows&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not leave noisy debugging permanently in production code.&lt;/p&gt;

&lt;p&gt;But during diagnosis, these checkpoints make it much easier to see where the transaction begins consuming its budget.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Limits That Mattered
&lt;/h2&gt;

&lt;p&gt;For synchronous Apex, several limits deserve immediate attention during this type of investigation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Synchronous Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SOQL queries&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOQL rows retrieved&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DML statements&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DML rows&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apex CPU time&lt;/td&gt;
&lt;td&gt;10,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Asynchronous Apex provides more room for certain limits, including a higher SOQL-query allowance and significantly more CPU time.&lt;/p&gt;

&lt;p&gt;But moving bad code asynchronous is not automatically a fix.&lt;/p&gt;

&lt;p&gt;You can still write an inefficient asynchronous transaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure #2: The Query Was Too Broad
&lt;/h2&gt;

&lt;p&gt;The next question was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why were we retrieving so many &lt;code&gt;Usage_Event__c&lt;/code&gt; records?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The query looked innocent:&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="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;Account__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Duration__c&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="n"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accountIds&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the data distribution mattered.&lt;/p&gt;

&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total Usage_Event__c records:     4,120,000
Processed__c = false:             1,480,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Processed__c = false&lt;/code&gt; is not narrowing the table very much.&lt;/p&gt;

&lt;p&gt;And several large customers might each own tens of thousands of usage events.&lt;/p&gt;

&lt;p&gt;A filter can be logically correct while still being operationally expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the Query Plan
&lt;/h3&gt;

&lt;p&gt;This is where I would stop guessing.&lt;/p&gt;

&lt;p&gt;Use Salesforce's Query Plan tooling and inspect how the optimizer expects to execute the SOQL.&lt;/p&gt;

&lt;p&gt;Things I want to know include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the query using an index?&lt;/li&gt;
&lt;li&gt;Is Salesforce considering a table scan?&lt;/li&gt;
&lt;li&gt;How many rows does the optimizer expect?&lt;/li&gt;
&lt;li&gt;What is the relative cost?&lt;/li&gt;
&lt;li&gt;Is one filter far less selective than expected?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Indexed does not automatically mean selective.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A field may have an index and still match so much of the table that using the index provides little benefit.&lt;/p&gt;

&lt;p&gt;With four million records, data distribution becomes part of application design.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Question: Why Are We Reading History?
&lt;/h2&gt;

&lt;p&gt;Then we found the more important design problem.&lt;/p&gt;

&lt;p&gt;Every Account update caused the application to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Give me every unprocessed usage event belonging to this Account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But most Account changes had nothing to do with usage.&lt;/p&gt;

&lt;p&gt;Changing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BillingCity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could trigger the same expensive calculation as changing a field that actually affected usage.&lt;/p&gt;

&lt;p&gt;That is wasted work.&lt;/p&gt;

&lt;p&gt;The first architectural improvement was therefore not another SOQL optimization.&lt;/p&gt;

&lt;p&gt;It was reducing &lt;strong&gt;when the calculation ran&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Only Execute When Relevant Fields Change
&lt;/h3&gt;

&lt;p&gt;Instead of recalculating on every Account update:&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="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;calculateUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&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;p&gt;we identified whether relevant fields actually changed.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;accountsNeedingRecalculation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;oldAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;oldMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&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="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Usage_Mode__c&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;oldAccount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Usage_Mode__c&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Subscription__c&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;oldAccount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Subscription__c&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;accountsNeedingRecalculation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Id&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;p&gt;Now an update to an unrelated field does not start an expensive calculation.&lt;/p&gt;

&lt;p&gt;Sometimes the cheapest query is the query you never run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure #3: CPU Became the Next Bottleneck
&lt;/h2&gt;

&lt;p&gt;After reducing the query volume, another test exposed a different failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.LimitException: Apex CPU time limit exceeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because retrieving data was only half of the problem.&lt;/p&gt;

&lt;p&gt;The code was also doing work like:&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="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Usage_Event__c&lt;/span&gt; &lt;span class="n"&gt;eventRecord&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// calculations&lt;/span&gt;
    &lt;span class="c1"&gt;// string transformations&lt;/span&gt;
    &lt;span class="c1"&gt;// map lookups&lt;/span&gt;
    &lt;span class="c1"&gt;// nested conditions&lt;/span&gt;
    &lt;span class="c1"&gt;// repeated aggregation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At thousands of iterations per Account, CPU consumption started climbing quickly.&lt;/p&gt;

&lt;p&gt;This is a common pattern when debugging governor limits.&lt;/p&gt;

&lt;p&gt;You fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SOQL queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then discover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then fix that and discover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPU
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Governor limits often expose inefficiencies layer by layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop Bringing Back Records You Only Want to Count
&lt;/h2&gt;

&lt;p&gt;This was another useful question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do we actually need the individual &lt;code&gt;Usage_Event__c&lt;/code&gt; records?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our example, we mainly needed totals.&lt;/p&gt;

&lt;p&gt;The original pattern effectively did this:&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="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;events&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;Account__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Duration__c&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;Decimal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;Decimal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;Usage_Event__c&lt;/span&gt; &lt;span class="n"&gt;eventRecord&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;containsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventRecord&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Account__c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventRecord&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Account__c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&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;totals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;eventRecord&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Account__c&lt;/span&gt;&lt;span class="p"&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;eventRecord&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Duration__c&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;p&gt;But the database is much better positioned to perform aggregation.&lt;/p&gt;

&lt;p&gt;Depending on the use case, an aggregate query may look more like:&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="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AggregateResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;results&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;Account__c&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration__c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;totalDuration&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="n"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accountIds&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Apex receives summary results rather than every matching row.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer objects in memory,&lt;/li&gt;
&lt;li&gt;less Apex iteration,&lt;/li&gt;
&lt;li&gt;simpler business logic,&lt;/li&gt;
&lt;li&gt;lower heap pressure,&lt;/li&gt;
&lt;li&gt;potentially lower CPU consumption.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An aggregate query is not magic.&lt;/p&gt;

&lt;p&gt;The underlying filters still need to scale.&lt;/p&gt;

&lt;p&gt;But it is wasteful to retrieve 30,000 records when the application only needs one number.&lt;/p&gt;




&lt;h2&gt;
  
  
  Add a Real Data Boundary
&lt;/h2&gt;

&lt;p&gt;The original query also had no meaningful time boundary.&lt;/p&gt;

&lt;p&gt;It asked for every matching record from years of history.&lt;/p&gt;

&lt;p&gt;But the business requirement only needed recent usage.&lt;/p&gt;

&lt;p&gt;So instead of:&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="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="n"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accountIds&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the query could potentially become something closer to:&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="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Account__c&lt;/span&gt; &lt;span class="n"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accountIds&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;Processed__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cutoffDate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CreatedDate&lt;/code&gt; is one of the fields Salesforce can index.&lt;/p&gt;

&lt;p&gt;More importantly, the new predicate represents an actual business boundary.&lt;/p&gt;

&lt;p&gt;Good query optimization is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How can I trick the optimizer?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What data does the transaction genuinely need?”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Backfills Do Not Belong in a User Transaction
&lt;/h2&gt;

&lt;p&gt;We still had another requirement.&lt;/p&gt;

&lt;p&gt;Historical usage needed recalculation across millions of records.&lt;/p&gt;

&lt;p&gt;Trying to do that during an ordinary record update would never be a healthy design.&lt;/p&gt;

&lt;p&gt;This was a different workload.&lt;/p&gt;

&lt;p&gt;So it deserved a different execution model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch Apex for the Historical Work
&lt;/h3&gt;

&lt;p&gt;Batch Apex is designed for processing large datasets in independent chunks.&lt;/p&gt;

&lt;p&gt;A simplified example:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsageBackfillBatch&lt;/span&gt;
    &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Batchable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SObject&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;QueryLocator&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;BatchableContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueryLocator&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;Account__c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Duration__c&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;startDate&lt;/span&gt;
            &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;endDate&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;BatchableContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Usage_Event__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;scope&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Process the current chunk.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;BatchableContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Logging, reconciliation, or next step.&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;p&gt;The important part is not simply that Batch Apex is asynchronous.&lt;/p&gt;

&lt;p&gt;The important part is the &lt;strong&gt;transaction boundary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each batch execution gets its own governor-limit context.&lt;/p&gt;

&lt;p&gt;Salesforce also allows a Batch Apex &lt;code&gt;Database.QueryLocator&lt;/code&gt; to work with very large result sets—up to tens of millions of records—making it much more appropriate for a four-million-record backfill than a normal synchronous transaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Queueable vs Batch
&lt;/h2&gt;

&lt;p&gt;A useful rule of thumb is:&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Queueable Apex When
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;work is reasonably bounded,&lt;/li&gt;
&lt;li&gt;you need asynchronous execution,&lt;/li&gt;
&lt;li&gt;jobs need chaining,&lt;/li&gt;
&lt;li&gt;you are processing a specific known workload.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Batch Apex When
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the dataset itself is large,&lt;/li&gt;
&lt;li&gt;millions of records may need processing,&lt;/li&gt;
&lt;li&gt;work needs to be split into independent chunks,&lt;/li&gt;
&lt;li&gt;limits need to reset between chunks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not move everything asynchronous simply because synchronous code reaches a limit.&lt;/p&gt;

&lt;p&gt;Choose the execution model based on the workload.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Better Architecture
&lt;/h2&gt;

&lt;p&gt;After working through the failures, the architecture looked very different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account Update
      ↓
Trigger
      ↓
Query historical usage
      ↓
Load thousands of records
      ↓
Calculate totals in Apex
      ↓
Update account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every transaction did far too much work.&lt;/p&gt;

&lt;h3&gt;
  
  
  After
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account Update
      ↓
Did a relevant field change?
      ↓
     Yes
      ↓
Query only required data
      ↓
Database-side aggregation
      ↓
Update required values
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Historical work followed a separate path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Historical Recalculation
      ↓
Batch Apex
      ↓
Selective QueryLocator
      ↓
Small execution scopes
      ↓
Process
      ↓
Reconcile results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation was much more important than any single code optimization.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Fixed the Governor Limits?
&lt;/h2&gt;

&lt;p&gt;There was no magic line of Apex.&lt;/p&gt;

&lt;p&gt;The fix was a combination of decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Removed SOQL From Loops
&lt;/h3&gt;

&lt;p&gt;This eliminated the immediate &lt;code&gt;101 queries&lt;/code&gt; failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reduced Unnecessary Execution
&lt;/h3&gt;

&lt;p&gt;The expensive logic stopped running for unrelated Account changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Made Queries More Selective
&lt;/h3&gt;

&lt;p&gt;We stopped treating the four-million-row object like a small lookup table.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Used Query Plan Instead of Guessing
&lt;/h3&gt;

&lt;p&gt;We verified how Salesforce expected to execute important SOQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Retrieved Less Data
&lt;/h3&gt;

&lt;p&gt;Transactions asked only for data required by the business operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Moved Aggregation Toward the Database
&lt;/h3&gt;

&lt;p&gt;Apex stopped iterating through large collections merely to calculate totals.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Separated Transactional Work From Backfills
&lt;/h3&gt;

&lt;p&gt;User-facing transactions remained small.&lt;/p&gt;

&lt;p&gt;Historical processing moved to Batch Apex.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Tested With Production-Like Data Volume
&lt;/h3&gt;

&lt;p&gt;The original implementation had looked fine because the development environment did not reproduce production cardinality.&lt;/p&gt;

&lt;p&gt;That last point deserves more attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Sandbox Can Lie to You
&lt;/h2&gt;

&lt;p&gt;Suppose a development sandbox contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12,000 Usage_Event__c records
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and production contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4,120,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same SOQL statement may behave very differently.&lt;/p&gt;

&lt;p&gt;A test can prove functional correctness while telling you almost nothing about large-volume performance.&lt;/p&gt;

&lt;p&gt;For important LDV paths, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is production row count?&lt;/li&gt;
&lt;li&gt;How are records distributed?&lt;/li&gt;
&lt;li&gt;Are a few parent records heavily skewed?&lt;/li&gt;
&lt;li&gt;How many records match common filters?&lt;/li&gt;
&lt;li&gt;Does the Query Plan change at production volume?&lt;/li&gt;
&lt;li&gt;What happens when 200 records enter the trigger?&lt;/li&gt;
&lt;li&gt;Which other automation shares the transaction?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance testing requires realistic &lt;strong&gt;cardinality&lt;/strong&gt;, not merely realistic field values.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Governor-Limit Debugging Checklist
&lt;/h2&gt;

&lt;p&gt;When I see an Apex limit exception, this is the order I would investigate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  SOQL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Is any query inside a loop?&lt;/li&gt;
&lt;li&gt;How many queries does the entire transaction execute?&lt;/li&gt;
&lt;li&gt;How many rows does each query return?&lt;/li&gt;
&lt;li&gt;Are the WHERE filters selective?&lt;/li&gt;
&lt;li&gt;What does Query Plan report?&lt;/li&gt;
&lt;li&gt;Are we retrieving fields or records we never use?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  DML
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Is DML happening inside loops?&lt;/li&gt;
&lt;li&gt;Can updates be collected and performed together?&lt;/li&gt;
&lt;li&gt;Are we updating records whose values did not actually change?&lt;/li&gt;
&lt;li&gt;Is DML recursively triggering additional automation?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CPU
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are there nested loops?&lt;/li&gt;
&lt;li&gt;Are large collections repeatedly scanned?&lt;/li&gt;
&lt;li&gt;Can Sets or Maps replace repeated searches?&lt;/li&gt;
&lt;li&gt;Is Flow or other automation consuming the same transaction?&lt;/li&gt;
&lt;li&gt;Are calculations being repeated unnecessarily?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Heap
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are we loading large record collections?&lt;/li&gt;
&lt;li&gt;Are we selecting large text fields we do not need?&lt;/li&gt;
&lt;li&gt;Can data be processed incrementally?&lt;/li&gt;
&lt;li&gt;Can aggregation happen in SOQL?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Does this work need to happen synchronously?&lt;/li&gt;
&lt;li&gt;Is this a transactional operation or a backfill?&lt;/li&gt;
&lt;li&gt;Would Queueable or Batch Apex fit better?&lt;/li&gt;
&lt;li&gt;Can the system process only changes instead of recalculating history?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The hardest part of debugging &lt;strong&gt;Apex governor limits&lt;/strong&gt; in a large Salesforce org is realizing that the limit exception is often not the real bug.&lt;/p&gt;

&lt;p&gt;The real bug may be an incorrect assumption.&lt;/p&gt;

&lt;p&gt;An assumption such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This table will stay small.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“An Account only has a few child records.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This trigger normally receives one record.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Bulkifying one query makes the solution scalable.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those assumptions can remain invisible for years.&lt;/p&gt;

&lt;p&gt;Then the organization reaches four million records and the architecture finally has enough data to prove them wrong.&lt;/p&gt;

&lt;p&gt;Governor limits are not just restrictions developers need to work around.&lt;/p&gt;

&lt;p&gt;They force us to design transactions with explicit resource boundaries.&lt;/p&gt;

&lt;p&gt;And at scale, that usually leads to better systems.&lt;/p&gt;

&lt;p&gt;These problems are why &lt;a href="https://sdlccorp.com/certified-salesforce-development-company/" rel="noopener noreferrer"&gt;scalable Salesforce development&lt;/a&gt; requires more than working Apex code; data volume, transaction boundaries, automation, and long-term platform behavior all need to be considered together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;If I had to reduce this debugging exercise to five rules, they would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Measure the whole transaction, not just your class.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bulkification is mandatory, but it is not the same as scalability.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query selectivity matters more as data volume grows.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not retrieve data that the transaction does not need.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move large historical workloads into execution models designed for them.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four million records should not automatically break Apex.&lt;/p&gt;

&lt;p&gt;But four million records will expose code that was designed as if the database would always remain small.&lt;/p&gt;

&lt;p&gt;And that is exactly why large-data-volume debugging is such a useful test of Salesforce architecture.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>What a Real Code Review Process Looks Like at Scale</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:47:46 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/what-a-real-code-review-process-looks-like-at-scale-5820</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/what-a-real-code-review-process-looks-like-at-scale-5820</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cf1axt03j2rlbircqvl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cf1axt03j2rlbircqvl.png" alt="What a Real Code Review Process Looks Like at Scale" width="800" height="535"&gt;&lt;/a&gt;&lt;br&gt;
On a five-person engineering team, code review can be simple.&lt;/p&gt;

&lt;p&gt;Open a pull request.&lt;br&gt;
Message a teammate.&lt;br&gt;
Get an approval.&lt;br&gt;
Merge.&lt;/p&gt;

&lt;p&gt;At fifty, one hundred, or five hundred engineers, that approach starts to break.&lt;/p&gt;

&lt;p&gt;Reviewers become bottlenecks. Pull requests sit untouched. Teams argue about style. Critical changes get the same level of attention as harmless configuration updates. Senior engineers spend hours reviewing code that automated tools could have checked in seconds.&lt;/p&gt;

&lt;p&gt;A mature &lt;strong&gt;code review process&lt;/strong&gt; is therefore not just about reading code before it reaches production.&lt;/p&gt;

&lt;p&gt;It is a system for deciding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What humans should review&lt;/li&gt;
&lt;li&gt;Who should review it&lt;/li&gt;
&lt;li&gt;What can be automated&lt;/li&gt;
&lt;li&gt;Which issues should block a merge&lt;/li&gt;
&lt;li&gt;How quickly reviews should happen&lt;/li&gt;
&lt;li&gt;How much scrutiny each change deserves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is what that process actually looks like at scale.&lt;/p&gt;


&lt;h2&gt;
  
  
  Code Review Starts Before the Pull Request
&lt;/h2&gt;

&lt;p&gt;One of the biggest mistakes teams make is treating review as something that begins after development finishes.&lt;/p&gt;

&lt;p&gt;By then, many expensive decisions have already been made.&lt;/p&gt;

&lt;p&gt;For meaningful changes, reviewers should understand the problem before debating individual lines of code.&lt;/p&gt;

&lt;p&gt;A strong pull request should explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What problem are we solving?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why is this change necessary?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What approach was chosen?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What risks does the change introduce?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How was it tested?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reviewer should not have to reverse-engineer the purpose of a change from a 700-line diff.&lt;/p&gt;

&lt;p&gt;For larger architectural decisions, the real review may begin even earlier through design documents, technical proposals, RFCs, or architecture discussions.&lt;/p&gt;

&lt;p&gt;Code review should validate implementation decisions.&lt;/p&gt;

&lt;p&gt;It should not become the first place where the team discovers what is being built.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Author Reviews First
&lt;/h2&gt;

&lt;p&gt;The first reviewer should always be the developer who wrote the code.&lt;/p&gt;

&lt;p&gt;Before requesting review, the author should inspect the complete diff exactly as another engineer will see it.&lt;/p&gt;

&lt;p&gt;That simple habit catches a surprising number of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging statements&lt;/li&gt;
&lt;li&gt;Accidental file changes&lt;/li&gt;
&lt;li&gt;Unclear names&lt;/li&gt;
&lt;li&gt;Duplicate logic&lt;/li&gt;
&lt;li&gt;Missing tests&lt;/li&gt;
&lt;li&gt;Commented-out code&lt;/li&gt;
&lt;li&gt;Unnecessary complexity&lt;/li&gt;
&lt;li&gt;Incorrect configuration&lt;/li&gt;
&lt;li&gt;Forgotten documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pull request should also contain enough context for someone who was not part of the implementation.&lt;/p&gt;

&lt;p&gt;A useful description might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Problem
Users can submit duplicate payment requests when the client retries.

Solution
Add an idempotency key to payment creation requests and persist processed keys.

Testing
- Added unit tests for duplicate requests
- Added integration test for retry behavior
- Tested timeout and retry scenarios locally

Risk
Database writes now include one additional lookup.

Rollback
Disable idempotency validation using the existing feature flag.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fix payment issue.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gives the reviewer a starting point.&lt;/p&gt;

&lt;p&gt;The other gives them detective work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automation Reviews the Mechanical Problems
&lt;/h2&gt;

&lt;p&gt;At scale, humans should not spend their review time pointing out formatting errors.&lt;/p&gt;

&lt;p&gt;That belongs to automation.&lt;/p&gt;

&lt;p&gt;Before a pull request reaches serious human review, teams should automate repetitive checks through CI, static analysis, security scanning, and a strong &lt;a href="https://sdlccorp.com/services/testing-services/" rel="noopener noreferrer"&gt;software testing and QA&lt;/a&gt; process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Formatting
    ↓
Linting
    ↓
Static Analysis
    ↓
Unit Tests
    ↓
Integration Tests
    ↓
Security Scanning
    ↓
Build Verification
    ↓
Human Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the system, automated gates may also check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency vulnerabilities&lt;/li&gt;
&lt;li&gt;Secrets accidentally committed&lt;/li&gt;
&lt;li&gt;Test coverage changes&lt;/li&gt;
&lt;li&gt;Database migration safety&lt;/li&gt;
&lt;li&gt;API compatibility&lt;/li&gt;
&lt;li&gt;Infrastructure policies&lt;/li&gt;
&lt;li&gt;License restrictions&lt;/li&gt;
&lt;li&gt;Type checking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This changes the role of the reviewer.&lt;/p&gt;

&lt;p&gt;Instead of writing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Please fix this formatting.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reviewer can spend time asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Should this service own this responsibility?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much better use of engineering attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reviewers Are Selected by Ownership, Not Availability
&lt;/h2&gt;

&lt;p&gt;Another process that fails at scale is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Whoever is online, please approve this.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Approval is not valuable simply because someone clicked the button.&lt;/p&gt;

&lt;p&gt;The reviewer needs enough context and expertise to judge the change.&lt;/p&gt;

&lt;p&gt;Different parts of a system may require different reviewers.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend Change
      ↓
Frontend Reviewer

Database Migration
      ↓
Database Reviewer

Authentication Logic
      ↓
Security-Aware Backend Reviewer

Cloud Infrastructure
      ↓
Platform / DevOps Reviewer

Shared API Contract
      ↓
Service Owner + Consuming Team
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Large engineering organizations often formalize this with ownership rules such as &lt;code&gt;CODEOWNERS&lt;/code&gt;, service ownership maps, or repository-level approval policies.&lt;/p&gt;

&lt;p&gt;The goal is not bureaucracy.&lt;/p&gt;

&lt;p&gt;The goal is getting the &lt;strong&gt;right eyes on the right risk&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not Every Pull Request Deserves the Same Process
&lt;/h2&gt;

&lt;p&gt;A two-line documentation correction should not require the same approval path as a change to authentication infrastructure.&lt;/p&gt;

&lt;p&gt;Scalable teams use &lt;strong&gt;risk-based review&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Low Risk
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Copy changes&lt;/li&gt;
&lt;li&gt;Tests&lt;/li&gt;
&lt;li&gt;Small internal refactors&lt;/li&gt;
&lt;li&gt;Non-production tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible requirement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One standard reviewer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Medium Risk
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;li&gt;Public APIs&lt;/li&gt;
&lt;li&gt;Database queries&lt;/li&gt;
&lt;li&gt;Significant UI behavior&lt;/li&gt;
&lt;li&gt;Service integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible requirement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Code owner or experienced reviewer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  High Risk
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Payments&lt;/li&gt;
&lt;li&gt;Encryption&lt;/li&gt;
&lt;li&gt;Database migrations&lt;/li&gt;
&lt;li&gt;Infrastructure&lt;/li&gt;
&lt;li&gt;Privacy-sensitive data&lt;/li&gt;
&lt;li&gt;Production-critical architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible requirement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Multiple reviewers or domain specialists.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This prevents two bad extremes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Review everything heavily&lt;/strong&gt; — which destroys engineering velocity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Review everything casually&lt;/strong&gt; — which increases production risk.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Small Pull Requests Are an Engineering Advantage
&lt;/h2&gt;

&lt;p&gt;Large pull requests are difficult to review properly.&lt;/p&gt;

&lt;p&gt;Imagine receiving this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2,840 lines changed across 47 files.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most reviewers are already tired before opening it.&lt;/p&gt;

&lt;p&gt;Large changes increase cognitive load. They also make it harder to identify the important part of the implementation.&lt;/p&gt;

&lt;p&gt;Smaller changes are usually easier to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand&lt;/li&gt;
&lt;li&gt;Review&lt;/li&gt;
&lt;li&gt;Test&lt;/li&gt;
&lt;li&gt;Discuss&lt;/li&gt;
&lt;li&gt;Merge&lt;/li&gt;
&lt;li&gt;Revert&lt;/li&gt;
&lt;li&gt;Debug later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A large feature can often be delivered as a sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PR 1 → Data Model
PR 2 → Internal Service
PR 3 → API
PR 4 → UI
PR 5 → Feature Enablement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature may still be large.&lt;/p&gt;

&lt;p&gt;The review units do not have to be.&lt;/p&gt;

&lt;p&gt;The goal is not to chase an arbitrary line-count limit.&lt;/p&gt;

&lt;p&gt;The goal is to create changes that a reviewer can understand without loading an entire project into their head.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Reviewer Should Read More Than the Diff
&lt;/h2&gt;

&lt;p&gt;A weak review focuses almost entirely on syntax.&lt;/p&gt;

&lt;p&gt;A Strong reviews go beyond syntax and challenge broader &lt;a href="https://sdlccorp.com/software-consulting-services/" rel="noopener noreferrer"&gt;software architecture and engineering decisions&lt;/a&gt; that can affect scalability, maintainability, and future development.&lt;/p&gt;

&lt;p&gt;A useful review sequence looks like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Understand the Intent
&lt;/h3&gt;

&lt;p&gt;Read the issue, description, design document, or requirement.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does this implementation actually solve the stated problem?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Review the Design
&lt;/h3&gt;

&lt;p&gt;Before inspecting every line, look at the architecture.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this responsibility in the correct layer?&lt;/li&gt;
&lt;li&gt;Are we introducing unnecessary coupling?&lt;/li&gt;
&lt;li&gt;Is there already an abstraction that solves this?&lt;/li&gt;
&lt;li&gt;Is the solution more complex than the problem?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Review Correctness
&lt;/h3&gt;

&lt;p&gt;Now inspect the implementation.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge cases&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Null states&lt;/li&gt;
&lt;li&gt;Concurrency&lt;/li&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Duplicate operations&lt;/li&gt;
&lt;li&gt;Failure recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Review Tests
&lt;/h3&gt;

&lt;p&gt;Do not check only whether tests exist.&lt;/p&gt;

&lt;p&gt;Ask whether they prove the behavior that matters.&lt;/p&gt;

&lt;p&gt;A test suite that always passes is not automatically useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Review Operational Impact
&lt;/h3&gt;

&lt;p&gt;Production code does not live inside a pull request.&lt;/p&gt;

&lt;p&gt;Think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Database load&lt;/li&gt;
&lt;li&gt;Backwards compatibility&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;li&gt;Rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Review Maintainability
&lt;/h3&gt;

&lt;p&gt;Finally, ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Will another engineer understand this six months from now?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question catches many problems that syntax-focused reviews miss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Review Comments Need Severity
&lt;/h2&gt;

&lt;p&gt;One of the most frustrating code review experiences is receiving fifteen comments without knowing which ones actually prevent approval.&lt;/p&gt;

&lt;p&gt;Teams can solve this by establishing a simple vocabulary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blocking
&lt;/h3&gt;

&lt;p&gt;Must change before merge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLOCKING: This endpoint allows the caller to update
resources without verifying ownership.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Question
&lt;/h3&gt;

&lt;p&gt;The reviewer needs clarification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QUESTION: What happens if the worker processes
this event twice?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Suggestion
&lt;/h3&gt;

&lt;p&gt;A possible improvement, but discussion may be appropriate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUGGESTION: Could this validation live in the
domain service instead of the controller?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nit
&lt;/h3&gt;

&lt;p&gt;Minor and non-blocking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NIT: This variable name could be slightly clearer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the author knows exactly where attention is required.&lt;/p&gt;

&lt;p&gt;More importantly, small preferences stop accidentally blocking useful code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Review the Code, Not the Developer
&lt;/h2&gt;

&lt;p&gt;Good review culture matters as much as review mechanics.&lt;/p&gt;

&lt;p&gt;Compare:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why did you write this in such a complicated way?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This branch introduces several states that seem difficult to reason about. Could we simplify the flow by returning early here?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second comment discusses the code.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Code review should create shared understanding, not defensiveness.&lt;/p&gt;

&lt;p&gt;Comments should explain &lt;strong&gt;why&lt;/strong&gt; something is a problem whenever possible.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Change this.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Prefer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This cache is shared across requests, so mutating it here could create inconsistent results under concurrent traffic. Could we keep this value request-scoped?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now the review transfers knowledge instead of simply issuing instructions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Review Speed Is Part of the Process
&lt;/h2&gt;

&lt;p&gt;A technically excellent review system can still fail if developers regularly wait two days for feedback.&lt;/p&gt;

&lt;p&gt;Slow reviews create several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers lose context&lt;/li&gt;
&lt;li&gt;Branches drift&lt;/li&gt;
&lt;li&gt;Merge conflicts increase&lt;/li&gt;
&lt;li&gt;Work in progress grows&lt;/li&gt;
&lt;li&gt;Releases slow down&lt;/li&gt;
&lt;li&gt;Engineers begin avoiding smaller improvements because review is painful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teams therefore need an expectation around response time.&lt;/p&gt;

&lt;p&gt;That does not mean interrupting engineers every ten minutes.&lt;/p&gt;

&lt;p&gt;It means code review is treated as &lt;strong&gt;team work&lt;/strong&gt;, rather than optional work performed when someone eventually has spare time.&lt;/p&gt;

&lt;p&gt;A healthy process might establish expectations for:&lt;/p&gt;

&lt;h3&gt;
  
  
  Time to First Review
&lt;/h3&gt;

&lt;p&gt;How long does a pull request wait before someone looks at it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Time Between Review Rounds
&lt;/h3&gt;

&lt;p&gt;After the author pushes changes, how quickly does the reviewer return?&lt;/p&gt;

&lt;h3&gt;
  
  
  Total Time to Merge
&lt;/h3&gt;

&lt;p&gt;How long do ordinary changes remain open?&lt;/p&gt;

&lt;p&gt;The exact targets depend on the organization.&lt;/p&gt;

&lt;p&gt;What matters is that review latency is visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Separate Human Judgment From Policy
&lt;/h2&gt;

&lt;p&gt;Suppose every reviewer repeatedly comments:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Please add a test.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eventually, that should probably become a pull request requirement.&lt;/p&gt;

&lt;p&gt;If reviewers repeatedly catch:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This repository must not import package X.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That may belong in static analysis.&lt;/p&gt;

&lt;p&gt;If every PR needs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Database team approval required.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That belongs in ownership rules.&lt;/p&gt;

&lt;p&gt;A mature &lt;strong&gt;code review process&lt;/strong&gt; converts repeated human instructions into automated policy whenever possible.&lt;/p&gt;

&lt;p&gt;Humans should spend their limited attention on problems that require judgment.&lt;/p&gt;

&lt;p&gt;Machines should enforce deterministic rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Practical Scalable Review Flow
&lt;/h2&gt;

&lt;p&gt;Putting everything together, a mature workflow may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer Writes Change
        ↓
Developer Self-Reviews
        ↓
PR Includes Context + Testing + Risk
        ↓
CI Runs Automated Checks
        ↓
Ownership Rules Select Reviewers
        ↓
Reviewer Checks Design + Correctness
        ↓
Domain Specialists Review High-Risk Areas
        ↓
Author Addresses Blocking Feedback
        ↓
Required Approvals Complete
        ↓
CI Passes Again
        ↓
Merge
        ↓
Deployment + Monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice something important.&lt;/p&gt;

&lt;p&gt;The reviewer is only one part of the system.&lt;/p&gt;

&lt;p&gt;That is what makes the process scalable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Should Teams Measure?
&lt;/h2&gt;

&lt;p&gt;You can improve a process much faster when you can see where it is failing.&lt;/p&gt;

&lt;p&gt;Useful signals include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What It Can Reveal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to first review&lt;/td&gt;
&lt;td&gt;Reviewer availability problems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to merge&lt;/td&gt;
&lt;td&gt;Overall process friction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review rounds&lt;/td&gt;
&lt;td&gt;Unclear requirements or weak initial submissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull request size&lt;/td&gt;
&lt;td&gt;Changes that are difficult to review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer workload&lt;/td&gt;
&lt;td&gt;Bottlenecks around senior engineers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reopened defects&lt;/td&gt;
&lt;td&gt;Problems escaping review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI failure rate&lt;/td&gt;
&lt;td&gt;Changes being submitted before they are ready&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Metrics require care.&lt;/p&gt;

&lt;p&gt;If developers are rewarded simply for reducing review time, they may approve too quickly.&lt;/p&gt;

&lt;p&gt;If teams are judged only by pull request size, developers may artificially split changes that should remain together.&lt;/p&gt;

&lt;p&gt;Metrics should reveal problems.&lt;/p&gt;

&lt;p&gt;They should not replace engineering judgment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Signs Your Code Review Process Is Not Scaling
&lt;/h2&gt;

&lt;p&gt;You probably have a process problem when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same two senior engineers review everything&lt;/li&gt;
&lt;li&gt;Pull requests regularly wait days for feedback&lt;/li&gt;
&lt;li&gt;Reviews contain mostly formatting comments&lt;/li&gt;
&lt;li&gt;Huge pull requests are normal&lt;/li&gt;
&lt;li&gt;Nobody knows who owns important parts of the codebase&lt;/li&gt;
&lt;li&gt;Approvals are treated as formalities&lt;/li&gt;
&lt;li&gt;Reviewers debate personal style preferences&lt;/li&gt;
&lt;li&gt;CI regularly fails after review begins&lt;/li&gt;
&lt;li&gt;Authors provide almost no context&lt;/li&gt;
&lt;li&gt;Security-sensitive code gets ordinary review&lt;/li&gt;
&lt;li&gt;Developers merge just to avoid another review round&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These problems rarely disappear by telling people to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Review better.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They require changes to the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal Is Not Perfect Code
&lt;/h2&gt;

&lt;p&gt;A review process should raise quality without making engineering impossible.&lt;/p&gt;

&lt;p&gt;Perfect code does not exist.&lt;/p&gt;

&lt;p&gt;A reviewer can almost always find another variable to rename, another abstraction to reconsider, or another edge case worth discussing.&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does this change leave the system healthier than it was before?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is yes, important risks are handled, tests provide reasonable confidence, and remaining comments are minor preferences, merging may be better than continuing the discussion indefinitely.&lt;/p&gt;

&lt;p&gt;Good engineering organizations protect quality.&lt;/p&gt;

&lt;p&gt;Great ones learn how to protect quality &lt;strong&gt;without stopping delivery&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A real &lt;strong&gt;code review process&lt;/strong&gt; at scale is not a mandatory approval button sitting between development and deployment.&lt;/p&gt;

&lt;p&gt;It is an engineering system, Authors provide context and review their own work. Automation removes mechanical problems, Ownership rules find the right reviewers.&lt;/p&gt;

&lt;p&gt;Reviewers focus on design, correctness, risk, tests, and maintainability. High-risk changes receive deeper scrutiny, Feedback clearly separates blockers from suggestions.&lt;/p&gt;

&lt;p&gt;And the organization pays attention to review speed as well as review quality. When those pieces work together, code review becomes more than a defect-catching exercise.&lt;/p&gt;

&lt;p&gt;It becomes one of the mechanisms through which an engineering organization maintains its architecture, spreads knowledge, develops engineers, and keeps a growing codebase understandable.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Five Architecture Decisions We Regret</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Sat, 29 Aug 2026 11:55:44 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/five-architecture-decisions-we-regret-2hgi</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/five-architecture-decisions-we-regret-2hgi</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc3iwlcll6r3b3z02o0sk.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc3iwlcll6r3b3z02o0sk.png" alt="Five Architecture Decisions We Regret" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
Architecture decisions rarely look terrible when we make them.&lt;/p&gt;

&lt;p&gt;Most begin with reasonable intentions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“We'll need this when we scale.”&lt;/li&gt;
&lt;li&gt;“This will keep everything flexible.”&lt;/li&gt;
&lt;li&gt;“Microservices will make teams independent.”&lt;/li&gt;
&lt;li&gt;“Let's create one shared layer so we don't duplicate code.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six months later, that “flexibility” can become the reason a simple feature touches twelve files and three services.&lt;/p&gt;

&lt;p&gt;Looking back at common &lt;strong&gt;architecture mistakes&lt;/strong&gt;, the painful ones usually aren't caused by choosing the wrong programming language or framework.&lt;/p&gt;

&lt;p&gt;They're caused by making the system more complicated than the problem required.&lt;/p&gt;

&lt;p&gt;Here are five decisions I would challenge much earlier.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Splitting Into Microservices Too Early
&lt;/h2&gt;

&lt;p&gt;The original reasoning sounds convincing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Independent deployments
+ Independent scaling
+ Clear ownership
+ Smaller services
= Better architecture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there's another side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service discovery
Network failures
Retries
Distributed tracing
Message queues
Deployment pipelines
Data consistency
Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A function call that once looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service A
   ↓ HTTP
Service B
   ↓ Database
   ↓ Event
Service C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every boundary is also a failure boundary.&lt;/p&gt;

&lt;p&gt;Microservices can be excellent when there is a real need for independent deployment, scaling, ownership, or isolation.&lt;/p&gt;

&lt;p&gt;But adopting them before understanding the domain can create a &lt;strong&gt;distributed monolith&lt;/strong&gt;: many deployable services that are still tightly coupled.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we'd choose instead
&lt;/h3&gt;

&lt;p&gt;Start with strong module boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
├── Users
├── Orders
├── Payments
├── Inventory
└── Notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep extraction possible.&lt;/p&gt;

&lt;p&gt;Then turn a module into a service when there is a measurable reason to do it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Sharing One Database Across Everything
&lt;/h2&gt;

&lt;p&gt;This one feels incredibly productive at first.&lt;/p&gt;

&lt;p&gt;Three services need customer information?&lt;/p&gt;

&lt;p&gt;Easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Service ─────┐
Order Service ────┼──→ Shared Database
Billing Service ──┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No duplicate data.&lt;/p&gt;

&lt;p&gt;No APIs.&lt;/p&gt;

&lt;p&gt;No synchronization.&lt;/p&gt;

&lt;p&gt;Then someone changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly three systems can break.&lt;/p&gt;

&lt;p&gt;The database schema has quietly become the API.&lt;/p&gt;

&lt;p&gt;That's one of the more expensive &lt;strong&gt;architecture mistakes&lt;/strong&gt; because the coupling isn't always visible in application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we'd do differently
&lt;/h3&gt;

&lt;p&gt;Make ownership explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Users Service
     ↓
Users Data

Orders Service
     ↓
Orders Data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other components should interact through deliberate interfaces rather than silently depending on internal tables.&lt;/p&gt;

&lt;p&gt;This doesn't mean every application immediately needs ten databases.&lt;/p&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Know who owns the data before everyone starts owning it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Architecture planning and data ownership are exactly the kinds of decisions worth examining during &lt;a href="https://sdlccorp.com/software-consulting-services/" rel="noopener noreferrer"&gt;software architecture consulting&lt;/a&gt; before implementation makes those boundaries expensive to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Building Abstractions Before We Needed Them
&lt;/h2&gt;

&lt;p&gt;Developers dislike duplication.&lt;/p&gt;

&lt;p&gt;Sometimes too much.&lt;/p&gt;

&lt;p&gt;Suppose two payment providers initially look similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;processStripePayment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;processPayPalPayment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we immediately design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AbstractPaymentProvider
        ↓
PaymentStrategyFactory
        ↓
PaymentProviderAdapter
        ↓
PaymentExecutionContext
        ↓
ConcreteProvider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beautiful diagram.&lt;/p&gt;

&lt;p&gt;Until the third provider behaves completely differently.&lt;/p&gt;

&lt;p&gt;Now we're fighting our abstraction instead of solving the business problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we'd do instead
&lt;/h3&gt;

&lt;p&gt;Accept small amounts of duplication while the domain is still becoming clear.&lt;/p&gt;

&lt;p&gt;Three similar implementations can teach us more than one premature abstraction.&lt;/p&gt;

&lt;p&gt;The better sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build
  ↓
Observe repetition
  ↓
Understand differences
  ↓
Extract abstraction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Predict future
  ↓
Build abstraction
  ↓
Hope reality agrees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good architecture removes unnecessary complexity.&lt;/p&gt;

&lt;p&gt;It doesn't hide complexity behind more interfaces.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Designing for Imaginary Scale
&lt;/h2&gt;

&lt;p&gt;“We should design this for 10 million users.”&lt;/p&gt;

&lt;p&gt;How many users do we currently have?&lt;/p&gt;

&lt;p&gt;“About 8,000.”&lt;/p&gt;

&lt;p&gt;Planning for growth is responsible.&lt;/p&gt;

&lt;p&gt;Designing every subsystem around hypothetical extreme scale is different.&lt;/p&gt;

&lt;p&gt;It can lead to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kafka
Redis
Elasticsearch
Multiple databases
Kubernetes
Microservices
Complex caching
Event-driven everything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before the product actually needs them.&lt;/p&gt;

&lt;p&gt;Now developers spend time operating architecture instead of improving the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we'd do differently
&lt;/h3&gt;

&lt;p&gt;Design for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current load
+
Expected near-term growth
+
A reasonable safety margin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then identify the parts that must remain replaceable.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Today

App
 ↓
PostgreSQL

Later

App
 ├── Cache
 ├── Read replicas
 ├── Search
 └── Queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple architecture with clean boundaries can evolve surprisingly far.&lt;/p&gt;

&lt;p&gt;The goal isn't to predict every future requirement.&lt;/p&gt;

&lt;p&gt;The goal is to avoid making future change unnecessarily expensive.&lt;/p&gt;

&lt;p&gt;That's also why maintainability and controlled growth matter in &lt;a href="https://sdlccorp.com/custom-software-development-services/" rel="noopener noreferrer"&gt;custom software development&lt;/a&gt;: scalability should be designed around actual product requirements rather than added as infrastructure for its own sake.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Treating Architecture Decisions as Permanent
&lt;/h2&gt;

&lt;p&gt;This may be the biggest regret.&lt;/p&gt;

&lt;p&gt;We once tended to think:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Choose architecture
       ↓
Build system
       ↓
Architecture finished
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real systems behave more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Choose
  ↓
Build
  ↓
Learn
  ↓
Measure
  ↓
Change
  ↓
Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The architecture that works for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 developers
10k users
1 product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may be wrong for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;50 developers
2M users
8 product lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That doesn't necessarily mean the original architecture failed.&lt;/p&gt;

&lt;p&gt;The context changed.&lt;/p&gt;

&lt;p&gt;The real mistake is refusing to reconsider the decision.&lt;/p&gt;




&lt;h1&gt;
  
  
  Architecture Decision Records Help
&lt;/h1&gt;

&lt;p&gt;One practice I would adopt earlier is recording important decisions.&lt;/p&gt;

&lt;p&gt;An ADR doesn't need to be complicated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Decision:
Use PostgreSQL as the primary datastore.

Context:
Small team, transactional workload,
no demonstrated need for multiple databases.

Why:
Operational simplicity and team experience.

Trade-offs:
Some future workloads may require
specialized storage.

Revisit when:
Database becomes a demonstrated bottleneck.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now future developers know &lt;strong&gt;why&lt;/strong&gt; something exists.&lt;/p&gt;

&lt;p&gt;Without that context, architecture can become archaeology.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Pattern Behind These Mistakes
&lt;/h1&gt;

&lt;p&gt;These five decisions look different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Premature microservices
Shared databases
Premature abstractions
Imaginary scale
Permanent decisions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But they have something in common.&lt;/p&gt;

&lt;p&gt;We optimized for a future we didn't understand yet.&lt;/p&gt;

&lt;p&gt;A better principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Make today's architecture simple while keeping tomorrow's important changes possible.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That doesn't mean ignoring scalability.&lt;/p&gt;

&lt;p&gt;It means earning complexity.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Better Architecture Checklist
&lt;/h1&gt;

&lt;p&gt;Before introducing a major architectural component, ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;□ What problem does this solve today?

□ Do we have evidence that the problem exists?

□ What new complexity does it introduce?

□ Who will operate it?

□ What happens when it fails?

□ Does it create additional coupling?

□ Can we postpone this decision?

□ How difficult will it be to reverse?

□ What would trigger us to reconsider it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we can't answer those questions, the architecture probably isn't ready.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Takeaway
&lt;/h1&gt;

&lt;p&gt;The architecture mistakes I worry about most aren't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wrong framework
Wrong database
Wrong cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They're decisions that create &lt;strong&gt;unnecessary coupling and complexity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A useful architecture doesn't need to predict the next five years perfectly.&lt;/p&gt;

&lt;p&gt;It needs to make today's system understandable and tomorrow's changes possible.&lt;/p&gt;

&lt;p&gt;So instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What is the most scalable architecture we can build?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“What is the simplest architecture that satisfies our current constraints without trapping us later?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question tends to produce much healthier systems.&lt;/p&gt;

&lt;p&gt;And usually fewer regrets.&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>Debugging Production Memory Leaks in a Node Monolith</title>
      <dc:creator>Samcorp</dc:creator>
      <pubDate>Sat, 29 Aug 2026 11:26:55 +0000</pubDate>
      <link>https://dev.to/samcorp_388df23e8f0e61ab6/debugging-production-memory-leaks-in-a-node-monolith-kaf</link>
      <guid>https://dev.to/samcorp_388df23e8f0e61ab6/debugging-production-memory-leaks-in-a-node-monolith-kaf</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zs0cawzomn0mjcc0ph4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zs0cawzomn0mjcc0ph4.png" alt="Debugging Production Memory Leaks in a Node Monolith" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph looked harmless at first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;09:00  → 420 MB
12:00  → 610 MB
15:00  → 790 MB
18:00  → 1.1 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traffic dropped.&lt;/p&gt;

&lt;p&gt;Memory didn't.&lt;/p&gt;

&lt;p&gt;Eventually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FATAL ERROR:
JavaScript heap out of memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a large Node.js monolith starts behaving like this, the tempting fix is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--max-old-space-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8192 app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes that buys time.&lt;/p&gt;

&lt;p&gt;It doesn't tell us &lt;strong&gt;why memory keeps growing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A useful &lt;strong&gt;Node memory leak&lt;/strong&gt; investigation starts by identifying &lt;em&gt;which kind of memory is growing&lt;/em&gt;, then finding what continues retaining it.&lt;/p&gt;




&lt;h2&gt;
  
  
  First: Confirm It's Actually a Leak
&lt;/h2&gt;

&lt;p&gt;Not every growing memory graph means leaked JavaScript objects.&lt;/p&gt;

&lt;p&gt;Start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;heapTotal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heapTotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;heapUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heapUsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;arrayBuffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arrayBuffers&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These numbers describe different things.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heapUsed
→ JavaScript objects currently using V8 heap

heapTotal
→ Heap currently allocated by V8

external
→ Memory associated with native/C++ objects

arrayBuffers
→ ArrayBuffer/SharedArrayBuffer memory,
  including Node Buffers

rss
→ Overall resident process memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction is important.&lt;/p&gt;

&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heapUsed ↑ continuously
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we may have retained JavaScript objects.&lt;/p&gt;

&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RSS ↑
heapUsed ≈ stable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we should also investigate Buffers, native modules, allocator behavior, or other memory outside the normal V8 heap.&lt;/p&gt;

&lt;p&gt;Don't diagnose from one metric.&lt;/p&gt;




&lt;h2&gt;
  
  
  Look at the Shape of Memory
&lt;/h2&gt;

&lt;p&gt;Healthy applications allocate memory constantly.&lt;/p&gt;

&lt;p&gt;Garbage collection should recover much of it.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory
  │      /\      /\      /\
  │     /  \    /  \    /  \
  │____/    \__/    \__/    \__
  └─────────────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory
  │             /\
  │          __/  \
  │       __/
  │    __/
  │___/
  └─────────────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The baseline keeps climbing after garbage collection.&lt;/p&gt;

&lt;p&gt;That's when I start asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is still referencing objects that should already be dead?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  In a Monolith, Start With Long-Lived Containers
&lt;/h1&gt;

&lt;p&gt;A large Node monolith usually has plenty of objects designed to live for the entire process lifetime.&lt;/p&gt;

&lt;p&gt;That makes them excellent leak candidates.&lt;/p&gt;

&lt;p&gt;Look at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Global Maps
Caches
Module-level arrays
Event emitters
Queues
Session stores
Timers
Request registries
WebSocket connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consider this innocent-looking cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;userCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;userCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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;p&gt;What's the eviction policy?&lt;/p&gt;

&lt;p&gt;There isn't one.&lt;/p&gt;

&lt;p&gt;If new IDs continue arriving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10,000 users
   ↓
50,000 users
   ↓
500,000 users
   ↓
Map keeps everything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically the objects are still reachable.&lt;/p&gt;

&lt;p&gt;So garbage collection is behaving correctly.&lt;/p&gt;

&lt;p&gt;The application is the thing leaking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watch Event Listeners
&lt;/h2&gt;

&lt;p&gt;Another common source of retention is listeners that are added repeatedly but never removed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;updateSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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;p&gt;Now imagine that happens for every request.&lt;/p&gt;

&lt;p&gt;The listener retains its closure.&lt;/p&gt;

&lt;p&gt;The closure may retain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
User
Metadata
Other objects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, lifecycle-sensitive listeners need lifecycle-sensitive cleanup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// process event&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Later&lt;/span&gt;
&lt;span class="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real question isn't just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How many listeners exist?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Why is this listener still alive?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Timers Can Quietly Retain Large Object Graphs
&lt;/h1&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;refreshCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&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;p&gt;That callback retains &lt;code&gt;customer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If jobs are repeatedly created without clearing old intervals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timer
  ↓
Closure
  ↓
Customer
  ↓
Associated objects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;remain reachable.&lt;/p&gt;

&lt;p&gt;Always know who owns a timer and when it ends.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// lifecycle ends&lt;/span&gt;
&lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes particularly important in monoliths because dozens of unrelated modules may create their own background jobs.&lt;/p&gt;




&lt;h1&gt;
  
  
  Take Heap Snapshots — Carefully
&lt;/h1&gt;

&lt;p&gt;Guessing eventually stops being useful.&lt;/p&gt;

&lt;p&gt;A better investigation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Snapshot A
    ↓
Generate representative traffic
    ↓
Wait / reproduce growth
    ↓
Snapshot B
    ↓
Compare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're looking for objects whose retained population or retained size keeps increasing.&lt;/p&gt;

&lt;p&gt;Typical suspects might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array
Map
Object
String
Closure
Buffer wrappers
Application-specific classes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect the retaining path.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Global Cache
   ↓
Map
   ↓
UserSession
   ↓
Orders[]
   ↓
Large response objects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have something actionable.&lt;/p&gt;

&lt;p&gt;Heap snapshots can temporarily consume significant CPU and memory, so capturing them blindly on a heavily loaded production instance is risky. Use a safe diagnostic strategy appropriate to your infrastructure.&lt;/p&gt;

&lt;p&gt;For long-lived Node applications, this kind of monitoring and troubleshooting belongs alongside ordinary &lt;a href="https://sdlccorp.com/node-js-development-company/" rel="noopener noreferrer"&gt;Node.js application development&lt;/a&gt; rather than being treated only as emergency work after an out-of-memory crash.&lt;/p&gt;




&lt;h1&gt;
  
  
  Don't Ignore Buffers
&lt;/h1&gt;

&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heapUsed = relatively stable
RSS      = climbing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JavaScript heap may not be the primary problem.&lt;/p&gt;

&lt;p&gt;Node applications handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File uploads
Image processing
Streams
Compression
Large API payloads
Sockets
Binary protocols
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;often work heavily with &lt;code&gt;Buffer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's why I monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;heapUsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;external&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;arrayBuffers&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;external&lt;/code&gt; or &lt;code&gt;arrayBuffers&lt;/code&gt; grows alongside RSS, the investigation changes direction.&lt;/p&gt;

&lt;p&gt;This is one reason simply increasing the V8 heap limit can completely miss the actual problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  Reproduce One Workload at a Time
&lt;/h1&gt;

&lt;p&gt;A monolith might handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REST API
Background jobs
Uploads
Reports
WebSockets
Emails
Scheduled tasks
Integrations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything runs simultaneously, identifying the leak becomes difficult.&lt;/p&gt;

&lt;p&gt;Isolate workloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Baseline
   ↓
Run API traffic
   ↓
Measure

Baseline
   ↓
Run report generation
   ↓
Measure

Baseline
   ↓
Run background jobs
   ↓
Measure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might discover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal API traffic
→ stable

WebSocket traffic
→ stable

Report generation
→ memory never returns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the search space is dramatically smaller.&lt;/p&gt;




&lt;h1&gt;
  
  
  Add Memory Observability Before the Next Incident
&lt;/h1&gt;

&lt;p&gt;At minimum, track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RSS
Heap used
Heap total
External memory
ArrayBuffer memory
Process restarts
OOM events
Request volume
Job volume
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then correlate memory growth with application behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory spike
     +
Report jobs increased
     +
Large Buffers increased
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is much more useful than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server uses 1.8 GB.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production performance should be treated as part of ongoing &lt;a href="https://sdlccorp.com/web-development-company/" rel="noopener noreferrer"&gt;web application maintenance and optimization&lt;/a&gt;, especially when a monolith has accumulated years of APIs, integrations, jobs, and shared application state.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Debugging Order I Prefer
&lt;/h1&gt;

&lt;p&gt;When investigating a &lt;strong&gt;Node memory leak&lt;/strong&gt;, I use roughly this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Confirm sustained growth
        ↓
2. Compare heapUsed vs RSS
        ↓
3. Check external / Buffer memory
        ↓
4. Correlate growth with workload
        ↓
5. Reproduce the suspicious path
        ↓
6. Capture safe heap snapshots
        ↓
7. Compare retained objects
        ↓
8. Follow retaining paths
        ↓
9. Fix ownership / cleanup
        ↓
10. Repeat the workload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask one final question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does memory return to a stable baseline?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If not, keep investigating.&lt;/p&gt;




&lt;h1&gt;
  
  
  Common Places I'd Check First
&lt;/h1&gt;

&lt;p&gt;For a production Node monolith:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;□ Unbounded Map/Set caches
□ Global arrays
□ Event listeners
□ setInterval/setTimeout
□ Request objects retained by closures
□ WebSocket connection state
□ Unfinished promises
□ Large Buffers
□ File-processing pipelines
□ In-memory sessions
□ Background queues
□ ORM/database result retention
□ Application metrics with unbounded labels
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that most leaks aren't mysterious V8 bugs.&lt;/p&gt;

&lt;p&gt;They're usually &lt;strong&gt;object ownership problems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Something was created.&lt;/p&gt;

&lt;p&gt;Something should have released it.&lt;/p&gt;

&lt;p&gt;Something didn't.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Takeaway
&lt;/h1&gt;

&lt;p&gt;The worst way to debug a Node memory problem is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory high
   ↓
Increase heap
   ↓
Restart server
   ↓
Wait
   ↓
Memory high again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A better approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MEASURE
   ↓
CLASSIFY
   ↓
REPRODUCE
   ↓
SNAPSHOT
   ↓
FIND RETAINER
   ↓
FIX
   ↓
MEASURE AGAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key lesson is that a &lt;strong&gt;Node memory leak&lt;/strong&gt; isn't simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Node is using too much RAM.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The useful question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Which memory is growing, and what is keeping it alive?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you can answer that, debugging stops being guesswork.&lt;/p&gt;

&lt;p&gt;And in a large monolith, reducing the search space is often half the fix.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
