<?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: Athreya aka Maneshwar</title>
    <description>The latest articles on DEV Community by Athreya aka Maneshwar (@lovestaco).</description>
    <link>https://dev.to/lovestaco</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%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png</url>
      <title>DEV Community: Athreya aka Maneshwar</title>
      <link>https://dev.to/lovestaco</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lovestaco"/>
    <language>en</language>
    <item>
      <title>PHP2Shell: Your Code Was Fine, The Engine Was Not</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:20:24 +0000</pubDate>
      <link>https://dev.to/lovestaco/php2shell-your-code-was-fine-the-engine-was-not-gc2</link>
      <guid>https://dev.to/lovestaco/php2shell-your-code-was-fine-the-engine-was-not-gc2</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine you do everything right.&lt;/p&gt;

&lt;p&gt;You validate your inputs. You use prepared statements. You escape your output. You have a linter, a code review process, and a slightly smug Slack channel called #security-wins.&lt;/p&gt;

&lt;p&gt;Then someone sends your server one weird HTTP request, and it runs their code instead of yours.&lt;/p&gt;

&lt;p&gt;Not because of anything you wrote. &lt;/p&gt;

&lt;p&gt;Because of something PHP did while cleaning up after itself.&lt;/p&gt;

&lt;p&gt;On October 21, 2026, the PHP project shipped a security release for two critical bugs, &lt;a href="https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/" rel="noopener noreferrer"&gt;CVE-2026-109618 and CVE-2026-109619&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The internet has already nicknamed the pair &lt;strong&gt;PHP2Shell&lt;/strong&gt;, which is a great name and a terrible Wednesday.&lt;/p&gt;

&lt;p&gt;Let's walk through what actually broke, why it is a bigger deal than "yet another CVE", and also where the scary headlines are a little ahead of the facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this one is different
&lt;/h2&gt;

&lt;p&gt;PHP is not a niche language. &lt;a href="https://w3techs.com/technologies/details/pl-php" rel="noopener noreferrer"&gt;W3Techs says 69.3% of websites whose server-side language it can identify run PHP&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;WordPress, Laravel, Drupal, Magento, that one invoice portal from 2014 that nobody dares touch. &lt;/p&gt;

&lt;p&gt;Most PHP vulnerabilities live in an application or a plugin. You patch the plugin, you move on.&lt;/p&gt;

&lt;p&gt;These two live in the &lt;strong&gt;engine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The engine is the thing underneath your code. It is the C program that parses your PHP, allocates your memory, and tears down after every request.&lt;/p&gt;

&lt;p&gt;You can write the most boring, correct PHP of your life, and the floor below it can still have a hole in it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[DIAGRAM PLACEHOLDER]&lt;/strong&gt; the stack, bottom to top: the web server (Apache or nginx), the SAPI (mod_php or PHP-FPM), the PHP engine, then your application code on top. Highlight the engine layer in red with the caption "both bugs live here, below everything you wrote". Show that an attacker's request has to pass through the engine before your code even sees it.&lt;/p&gt;
&lt;/blockquote&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%2F42rvmh0tzzh6uwdn97pf.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%2F42rvmh0tzzh6uwdn97pf.png" alt="Always Has Been meme: wait, my code was fine? always has been, my app and the engine" width="360" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both bugs are about the same unglamorous thing: &lt;strong&gt;memory management around a request that goes wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To see why, you need to know how PHP spends its life.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP's life is one request long
&lt;/h2&gt;

&lt;p&gt;PHP's model is famously "shared nothing". &lt;/p&gt;

&lt;p&gt;A request comes in, PHP sets up a fresh little world for it, runs your script, then throws the whole world away. &lt;/p&gt;

&lt;p&gt;The worker process survives, but the request's state is supposed to die with the request.&lt;/p&gt;

&lt;p&gt;That is why PHP is so forgiving. Leak some memory in a script? Nobody cares, the request ends and the memory gets reclaimed.&lt;/p&gt;

&lt;p&gt;Here is the happy path:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A([Request arrives]):::start --&amp;gt; B[Request startup&amp;lt;br/&amp;gt;build per-request state]:::engine
    B --&amp;gt; C[Run your script]:::app
    C --&amp;gt; D[Request shutdown&amp;lt;br/&amp;gt;cleanup and free]:::engine
    D --&amp;gt; E([Worker waits for the next request]):::start
    E --&amp;gt; A

    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef engine fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef app fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Notice that the purple boxes, startup and shutdown, are the engine's job. &lt;/p&gt;

&lt;p&gt;Your code only owns the teal one in the middle. &lt;/p&gt;

&lt;p&gt;Both vulnerabilities are about what happens when something goes wrong &lt;em&gt;between&lt;/em&gt; those purple boxes, and the engine's cleanup code assumes a state that no longer holds.&lt;/p&gt;

&lt;p&gt;PHP has a hard ceiling on how much memory one request may use, &lt;code&gt;memory_limit&lt;/code&gt;. Hit it, and PHP gives up on the request on the spot.&lt;/p&gt;

&lt;p&gt;"Gives up on the spot" is where the trouble starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: cleaning up a house that was never built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CVE-2026-109618, the out-of-memory one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the scenario, paraphrasing &lt;a href="https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/" rel="noopener noreferrer"&gt;the PHP Foundation's write-up&lt;/a&gt; and &lt;a href="https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/" rel="noopener noreferrer"&gt;Rapid7's analysis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;PHP is in the middle of request startup. It is building that per-request state, piece by piece.&lt;/p&gt;

&lt;p&gt;A memory allocation fails. PHP abandons the request abruptly.&lt;/p&gt;

&lt;p&gt;But the state is only &lt;em&gt;half&lt;/em&gt; built. Some fields are set, some are whatever bytes happened to be sitting in that memory.&lt;/p&gt;

&lt;p&gt;Then the cleanup code runs, and it does what cleanup code does: it trusts the state it is handed.&lt;/p&gt;

&lt;p&gt;It follows pointers that were never initialized. It frees things that were never allocated. It treats garbage as a structure.&lt;/p&gt;

&lt;p&gt;The mental model I like: a demolition crew is sent to tear down a building. &lt;/p&gt;

&lt;p&gt;The building was never finished, and the blueprint is a napkin with random scribbles on it. &lt;/p&gt;

&lt;p&gt;The crew follows the napkin anyway, and demolishes whatever the scribbles point at.&lt;/p&gt;

&lt;p&gt;Here is the shape of the bug as a deliberately simplified sketch. This is &lt;strong&gt;not&lt;/strong&gt; PHP's real source, just the pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified illustration of the bug class, not actual PHP code&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;request_startup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alloc_table&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// ok&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;uploads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alloc_table&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// out of memory! bail out...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;uploads&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;FAILURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;hooks&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alloc_table&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// never reached, r-&amp;gt;hooks is junk&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;request_shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;free_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;free_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;free_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// frees a pointer nobody ever set&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The mistake is not in either function alone. It is in the assumption that &lt;strong&gt;shutdown only ever runs on a fully started request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now here is the part that makes this one nasty.&lt;/p&gt;

&lt;p&gt;How do you make a server run out of memory on purpose? You do not need to hack anything, you just ask nicely and badly. &lt;/p&gt;

&lt;p&gt;Rapid7 notes that crafted multipart requests can exhaust memory even when their overall size looks like it fits inside the configured limits.&lt;/p&gt;

&lt;p&gt;And on Apache with &lt;code&gt;mod_php&lt;/code&gt;, the demonstrated attack happens &lt;strong&gt;before any of your application code runs.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;That is the whole reason the "zero authentication" line is true. There is no login to bypass. Your code never got the chance to say no.&lt;/p&gt;

&lt;p&gt;Memory corruption you can trigger with a request, in a place your app cannot guard, is how you end up with "9.2 critical" in a headline.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug 2: the ghost of requests past
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CVE-2026-109619, the stale state one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same family, different flavour.&lt;/p&gt;

&lt;p&gt;Remember that a worker process handles request after request. Some bookkeeping state is global to the worker, and is supposed to be reset between requests.&lt;/p&gt;

&lt;p&gt;Now picture this sequence:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant A as Request A
    participant W as PHP worker
    participant B as Request B
    A-&amp;gt;&amp;gt;W: runs, global state points at memory M
    W-&amp;gt;&amp;gt;W: fatal error interrupts cleanup
    W--&amp;gt;&amp;gt;W: M is freed, but the pointer to M is not cleared
    B-&amp;gt;&amp;gt;W: arrives on the same worker
    W-&amp;gt;&amp;gt;W: reads the old pointer before init resets it
    Note over W: use-after-free: B is reading memory it does not own&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;A fatal error, like running out of memory or a timeout, can interrupt the cleanup in the middle. &lt;/p&gt;

&lt;p&gt;The memory gets freed, but a pointer to it survives. The next request lands on the same worker, and something reads that pointer before normal initialization has a chance to wipe it.&lt;/p&gt;

&lt;p&gt;That is a classic &lt;strong&gt;use-after-free&lt;/strong&gt;. The name is the best part, because the thing you are using after it is free is not free at all, it is very expensive.&lt;/p&gt;

&lt;p&gt;Think of a hotel room that was demolished but the key card still opens the door. &lt;/p&gt;

&lt;p&gt;Whoever is standing there next gets to decide what is inside.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/" rel="noopener noreferrer"&gt;Rapid7 describes two concrete paths&lt;/a&gt; to reach it. &lt;/p&gt;

&lt;p&gt;One is the PHP 8.5 fatal error backtrace feature, which is on by default and was confirmed on &lt;code&gt;mod_php&lt;/code&gt;. The other is a session upload progress path that works on PHP-FPM, and needs an application operation to leave state behind first.&lt;/p&gt;

&lt;p&gt;Note the pattern. Neither bug is about a missing bounds check in some parser. They are both about &lt;strong&gt;error paths&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The happy path of PHP has been tested for decades. The path where a request dies at the worst possible moment is the one that gets less love.&lt;/p&gt;
&lt;h2&gt;
  
  
  So can anyone hack 70% of the internet?
&lt;/h2&gt;

&lt;p&gt;Short answer: no, and it is worth being precise, because the headline version of this story sells more than it delivers.&lt;/p&gt;

&lt;p&gt;What is solid:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both bugs are real and rated 9.2 (CVSS 4.0).&lt;/strong&gt; Both can lead to code execution as the web worker's user, with no authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RCE was demonstrated by researchers&lt;/strong&gt;, against real setups, not just crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;mod_php&lt;/code&gt; is the highest risk.&lt;/strong&gt; Rapid7 notes that for the OOM bug, &lt;code&gt;mod_php&lt;/code&gt; runs cleanup even when startup failed. &lt;a href="https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/" rel="noopener noreferrer"&gt;PHP-FPM kills the worker when startup fails&lt;/a&gt;, which blocks that specific sequence.&lt;/p&gt;

&lt;p&gt;What is more nuanced:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Any PHP server" is too strong.&lt;/strong&gt; Rapid7 reviewed multiple public proof of concepts, and each one works only for a specific combination of OS, PHP version and web server. None is broadly reliable yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The attack complexity is rated High.&lt;/strong&gt; Memory corruption exploits are fiddly. That is a speed bump, not a wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reports of exploitation are mixed.&lt;/strong&gt; &lt;a href="https://watchtowr.com/intelligence/php-cve-2026-109618-faq/" rel="noopener noreferrer"&gt;watchTowr says it has seen exploitation in the wild through its honeypots&lt;/a&gt;, while Rapid7 has seen attempts but not confirmed in-the-wild compromise. &lt;/p&gt;

&lt;p&gt;Whichever way that lands, Rapid7 also warns that a more portable technique could show up quickly.&lt;/p&gt;

&lt;p&gt;And the exposed surface is not small. &lt;a href="https://unit42.paloaltonetworks.com/cve-2026-109618-cve-2026-109619/" rel="noopener noreferrer"&gt;Palo Alto's Unit 42 brief&lt;/a&gt; cites telemetry that counts over 22 million potentially vulnerable internet-facing instances. That is a vendor number, so season to taste, but it is the right order of magnitude for "a lot".&lt;/p&gt;

&lt;p&gt;If I had to summarize it: not the end of the web, but also not a drill.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that is actually uncomfortable
&lt;/h2&gt;

&lt;p&gt;Here is the story behind the patch, and it is the most human part of the whole thing.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[2024&amp;lt;br/&amp;gt;Quarkslab audit,&amp;lt;br/&amp;gt;first signs of the OOM issue]:::early --&amp;gt; B[Fix stalls&amp;lt;br/&amp;gt;competing patches,&amp;lt;br/&amp;gt;each with trade-offs]:::slow
    B --&amp;gt; C[Aug 2026&amp;lt;br/&amp;gt;Calif, OpenAI and Wiz&amp;lt;br/&amp;gt;report it, RCE shown]:::report
    C --&amp;gt; D[Oct 20&amp;lt;br/&amp;gt;CVE IDs leak&amp;lt;br/&amp;gt;2 days early]:::bad
    D --&amp;gt; E[Oct 21&amp;lt;br/&amp;gt;Chainguard images&amp;lt;br/&amp;gt;break the embargo]:::bad
    E --&amp;gt; F[Oct 21, 14:00 UTC&amp;lt;br/&amp;gt;patched releases ship]:::good

    classDef early fill:#6ea8ff,stroke:#3a6fcc,color:#1a1a1a
    classDef slow fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef report fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef bad fill:#ff9a5c,stroke:#cc6a2a,color:#1a1a1a
    classDef good fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Per &lt;a href="https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/" rel="noopener noreferrer"&gt;the PHP Foundation&lt;/a&gt;, an external audit by Quarkslab back in 2024 led a core developer to spot the first signs of the OOM problem while fixing something else. &lt;/p&gt;

&lt;p&gt;No exploit path was known at the time.&lt;/p&gt;

&lt;p&gt;Then the fix stalled. Competing patches each had a cost: breaking compatibility, opening new denial of service angles, or touching lots of third-party extensions.&lt;/p&gt;

&lt;p&gt;The Foundation's own words are refreshingly blunt: the delay was "a structural failure, caused by both the security team's decision process and its lack of resources."&lt;/p&gt;

&lt;p&gt;Then in August 2026 the bug was reported again, with a working unconditional RCE. The CVE IDs leaked online before the release. The embargo then cracked a second time when a vendor published images that already contained the fix.&lt;/p&gt;

&lt;p&gt;I find this oddly reassuring and deeply annoying at the same time.&lt;/p&gt;

&lt;p&gt;Reassuring, because an open source project wrote that sentence about itself in public, and is changing its process: majority decisions, an ecosystem security team, more staff.&lt;/p&gt;

&lt;p&gt;Annoying, because "everyone who maintains critical infrastructure is under-resourced" is not a new lesson, and PHP is carrying a very large share of the web on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The AI angle, and where I would push back
&lt;/h2&gt;

&lt;p&gt;A lot of the commentary around this release points at one thing: AI changes who can exploit this.&lt;/p&gt;

&lt;p&gt;The argument goes like this. Turning a subtle memory corruption bug into a working attack used to need rare skills. Now somebody with a laptop and an open-weights model can point an agent at the internet, hand it the advisory and say "go find the ones that are still vulnerable".&lt;/p&gt;

&lt;p&gt;I think the direction is right and the details are overstated.&lt;/p&gt;

&lt;p&gt;What is right: &lt;strong&gt;the cost of going from advisory to attempt is falling fast.&lt;/strong&gt; The patch itself is a diff. A diff tells a model exactly where to look. And scanning is cheap when the scanner does not get tired.&lt;/p&gt;

&lt;p&gt;What is overstated: the hard part of &lt;em&gt;this&lt;/em&gt; bug is not finding targets, it is the exploit reliability. Rapid7 says the public PoCs are fragile. Memory corruption is a craft, and models are not magic yet at crafting heap layouts for your particular distro.&lt;/p&gt;

&lt;p&gt;And there is a side of the story that gets less airtime: the defenders have the same tools.&lt;/p&gt;

&lt;p&gt;The Foundation says it used AI-assisted scanning and validation as part of its security work, with the line that "a person makes every call and works on every patch."&lt;/p&gt;

&lt;p&gt;So the realistic summary is not "attackers got superpowers". It is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The window between "patch published" and "someone is trying it on your server" is shrinking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your patching speed is now a security control, not an IT chore.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;Boring answer first: &lt;strong&gt;update.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fixed releases are &lt;strong&gt;PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;If you are on 8.1 or older, there are no routine upstream releases. Rapid7 notes limited source backports exist for 7.4, 8.0 and 8.1, so lean on your distribution or an LTS vendor, or finally take the upgrade you have been postponing since 2022.&lt;/p&gt;

&lt;p&gt;Two gotchas people will trip on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A patched package on disk is not a patched process in memory.&lt;/strong&gt; Running workers keep the old code until they restart. &lt;a href="https://blog.qualys.com/vulnerabilities-threat-research/2026/10/20/php-announces-critical-security-release-for-two-vulnerabilities" rel="noopener noreferrer"&gt;Qualys specifically calls this out&lt;/a&gt;, so restart or gracefully reload your PHP services after upgrading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Containers do not save you from stale base images.&lt;/strong&gt; If your image was built last week, it is still carrying last week's PHP.&lt;/p&gt;

&lt;p&gt;Quick check:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt;                       &lt;span class="c"&gt;# compare against 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35&lt;/span&gt;

&lt;span class="c"&gt;# then make sure the running workers are the new ones&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload php8.3-fpm     &lt;span class="c"&gt;# or: apachectl graceful&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you truly cannot patch yet, the advisories list temporary mitigations. They reduce exposure, they do not fix it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; php.ini or FPM pool config, NOT ini_set() or .user.ini (too late for these)
&lt;/span&gt;&lt;span class="py"&gt;session.upload_progress.enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0   ; closes the known session path&lt;/span&gt;
&lt;span class="py"&gt;fatal_error_backtraces&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0            ; PHP 8.5 only&lt;/span&gt;
&lt;span class="py"&gt;post_max_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8M                    ; and cap multipart size at the frontend too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You will see &lt;code&gt;memory_limit=-1&lt;/code&gt; suggested too, and here is where I have to confess the Roll Safe meme is doing real work:&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%2Fp8vur48r4sclqt6qxhum.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%2Fp8vur48r4sclqt6qxhum.png" alt="Roll Safe meme: can't run out of memory if you turn off the memory limit" width="360" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It genuinely appears in the advisory guidance, set administratively so apps cannot override it. But Rapid7 is clear that an allocation failure can still reach the vulnerable path, so it narrows the window and does not close it. &lt;/p&gt;

&lt;p&gt;Also, a server with no memory limit has its own personality. It is called "an outage".&lt;/p&gt;

&lt;p&gt;And two things &lt;strong&gt;not&lt;/strong&gt; to reach for. Rapid7 warns that &lt;code&gt;RLIMIT_AS&lt;/code&gt; and &lt;code&gt;RLIMIT_DATA&lt;/code&gt; can themselves trigger the vulnerable allocation failure path, and container or cgroup memory limits are an incomplete answer and can kill the whole worker pool.&lt;/p&gt;

&lt;p&gt;Do not hide the evidence either. Silencing errors with &lt;code&gt;display_errors&lt;/code&gt; or &lt;code&gt;@&lt;/code&gt; does not prevent stale state from being read, and a quiet log is not proof you are fine.&lt;/p&gt;

&lt;p&gt;Here is the whole decision as a flowchart, for the person who has a patch window and a manager:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A([Do you run PHP on a web server?]):::start --&amp;gt; B{Is it on 8.2.35, 8.3.36,&amp;lt;br/&amp;gt;8.4.27 or 8.5.12 or newer?}:::decision
    B -- yes --&amp;gt; C{Did you restart or reload&amp;lt;br/&amp;gt;workers after upgrading?}:::decision
    C -- yes --&amp;gt; D([You are patched. Go touch grass.]):::good
    C -- no --&amp;gt; E[Restart workers now]:::action
    B -- no --&amp;gt; F{Is it 8.1 or older?}:::decision
    F -- yes --&amp;gt; G[Distro or LTS backport,&amp;lt;br/&amp;gt;or plan the upgrade]:::action
    F -- no --&amp;gt; H[Upgrade to a fixed release]:::action
    G --&amp;gt; I[Apply interim mitigations&amp;lt;br/&amp;gt;until it lands]:::warn
    H --&amp;gt; E

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef good fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef action fill:#6ea8ff,stroke:#3a6fcc,color:#1a1a1a
    classDef warn fill:#ff9a5c,stroke:#cc6a2a,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;


&lt;h2&gt;
  
  
  The thing worth taking away
&lt;/h2&gt;

&lt;p&gt;The patch is the urgent lesson. The durable one is quieter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your security posture includes layers you did not write and never read.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your code can be perfect and the runtime under it can still ship a bug that was first noticed two years ago. &lt;/p&gt;

&lt;p&gt;That is not a reason to panic about PHP specifically. Every runtime has error paths nobody loves, and the next bug of this kind will be in a different one.&lt;/p&gt;

&lt;p&gt;It is a reason to treat runtime and base image updates as first class work, to know which version is actually running in production, and to keep the gap between "advisory" and "deployed" as small as you can.&lt;/p&gt;

&lt;p&gt;Cleanup code, it turns out, is also code.&lt;/p&gt;

&lt;p&gt;Maybe even the code most worth reviewing.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Risk-Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is built for engineering teams that ship often, but also care about keeping production stable for customers. It approves some 40-50% of low-risk changes automatically, and it gives your team the tools to handle the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand risky changes faster.&lt;/li&gt;
&lt;li&gt;Fix problems in those changes, with human attention directed to the work that needs it.&lt;/li&gt;
&lt;li&gt;Enforce your engineering standards.&lt;/li&gt;
&lt;li&gt;Learn from production outcomes, such as incidents, rollbacks, and queue wait times.&lt;/li&gt;
&lt;li&gt;Improve the review process over time and make it more efficient.&lt;/li&gt;
&lt;li&gt;Connect to the major engineering tools that your team already uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Auto-Approve: Ship the Safe 50% Without a Human&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Auto-Approve Upto 50% of Your Pull Requests, Safely.&lt;/strong&gt; Risk scoring decides which MRs/PRs are safe enough to ship without a human, so your team only spends review time on risky and critical changes.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;demo.mp4&lt;/span&gt;
    
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview auto-approving a low-risk change while&lt;/i&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Processes vs Threads, as the Linux Kernel Sees Them</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:59:17 +0000</pubDate>
      <link>https://dev.to/lovestaco/processes-vs-threads-as-the-linux-kernel-sees-them-55pk</link>
      <guid>https://dev.to/lovestaco/processes-vs-threads-as-the-linux-kernel-sees-them-55pk</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"What is the difference between a process and a thread?"&lt;/p&gt;

&lt;p&gt;You have been asked this. You have probably asked it.&lt;/p&gt;

&lt;p&gt;The standard answer goes: a process is heavy, a thread is light, and threads share memory.&lt;/p&gt;

&lt;p&gt;That answer is true.&lt;/p&gt;

&lt;p&gt;It is also the kind of true that you can recite for ten years without ever understanding what is going on underneath.&lt;/p&gt;

&lt;p&gt;So let's skip the textbook and look at both from the seat where the decisions actually get made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel's seat.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one long bar of RAM
&lt;/h2&gt;

&lt;p&gt;Picture your machine's memory as a single long bar. Say 16 GB.&lt;/p&gt;

&lt;p&gt;All of it belongs to the kernel. No program is allowed to touch it directly.&lt;/p&gt;

&lt;p&gt;When you launch an app, the kernel creates a &lt;strong&gt;process&lt;/strong&gt; to represent it, and carves out a slice of the bar as that process's private memory.&lt;/p&gt;

&lt;p&gt;It writes down who owns which slice in a notebook, and it guards those boundaries.&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%2F5qev9zrnxn5p0irmy7s2.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%2F5qev9zrnxn5p0irmy7s2.png" alt="Diagram: a 16 GB RAM bar split into a kernel region, a photo editor slice, a Spotify slice and free space, with the kernel's notebook mapping each process to its own slice and an editor reaching into Spotify's slice being blocked" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now watch it with real software.&lt;/p&gt;

&lt;p&gt;You open a photo editor. The kernel makes a process for it, and the editor fills its slice with a 5 MB photo.&lt;/p&gt;

&lt;p&gt;You also have Spotify open. That is a second process, with a second slice further down the bar.&lt;/p&gt;

&lt;p&gt;The editor cannot read Spotify's memory. Spotify cannot read your photo.&lt;/p&gt;

&lt;p&gt;If the editor reaches outside its slice, the kernel stops it on the spot.&lt;/p&gt;

&lt;p&gt;And if Spotify crashes, your photo does not even flinch.&lt;/p&gt;

&lt;p&gt;That wall is the entire reason one program cannot read or corrupt another's data.&lt;/p&gt;

&lt;h3&gt;
  
  
  A small confession about the bar
&lt;/h3&gt;

&lt;p&gt;The bar is a &lt;em&gt;teaching&lt;/em&gt; picture, and it is a little too tidy.&lt;/p&gt;

&lt;p&gt;Real kernels do not hand out literal chunks of physical RAM. Every process gets its &lt;strong&gt;own virtual address space&lt;/strong&gt;, and the CPU's memory management unit translates those addresses through page tables that the kernel controls.&lt;/p&gt;

&lt;p&gt;Two processes can both use "address 0x1000" and land on completely different physical memory.&lt;/p&gt;

&lt;p&gt;Touch an address that is not mapped for you, and the hardware raises a fault, and the kernel typically answers with a &lt;code&gt;SIGSEGV&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/concepts.html" rel="noopener noreferrer"&gt;kernel's own memory docs&lt;/a&gt; cover the concepts if you want the full tour.&lt;/p&gt;

&lt;p&gt;The wall in the picture is a very real wall. It is just made of page tables instead of drywall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the editor wants to be fast
&lt;/h2&gt;

&lt;p&gt;Brightening a photo one pixel at a time on a single core is slow.&lt;/p&gt;

&lt;p&gt;So the editor asks the kernel for help: "give me four threads".&lt;/p&gt;

&lt;p&gt;Here is the part that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel does not hand each thread more RAM.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It creates four lightweight tasks and points all of them at the &lt;em&gt;same slice&lt;/em&gt; the process already owns.&lt;/p&gt;

&lt;p&gt;Then it schedules those four tasks onto four cores, and they run at the same moment.&lt;/p&gt;

&lt;p&gt;Each thread takes a quarter of the photo and brightens its own region, reading and writing the pixels directly where they sit.&lt;/p&gt;

&lt;p&gt;Nothing was copied.&lt;/p&gt;

&lt;p&gt;When the last thread finishes, the whole image is already brighter.&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%2Fyzsjfh6airl7wjzhbn44.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%2Fyzsjfh6airl7wjzhbn44.png" alt="Diagram: the same photo-brightening job split across four threads sharing one 5 MB slice, versus four processes each holding a full copy for 20 MB total plus a step to glue the results back together" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now imagine doing the same job with four &lt;strong&gt;processes&lt;/strong&gt; instead.&lt;/p&gt;

&lt;p&gt;Each one needs its own slice. So the photo gets copied into all four, which is four times the memory.&lt;/p&gt;

&lt;p&gt;Then somebody has to collect the four finished pieces and stitch them back together, which means sending data between processes with pipes, sockets or shared memory.&lt;/p&gt;

&lt;p&gt;That is the entire pitch for threads in one sentence: &lt;strong&gt;they skip the copy, and they skip the stitching.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A fairness note for processes
&lt;/h3&gt;

&lt;p&gt;I have been a bit mean to processes, so let me correct the record.&lt;/p&gt;

&lt;p&gt;Modern &lt;code&gt;fork()&lt;/code&gt; does not actually copy the whole photo up front. On Linux it is &lt;a href="https://man7.org/linux/man-pages/man2/fork.2.html" rel="noopener noreferrer"&gt;implemented with copy-on-write pages&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant P as parent
    participant K as kernel
    participant C as child

    P-&amp;gt;&amp;gt;K: fork()
    K--&amp;gt;&amp;gt;C: new task, page tables copied, pages not copied
    Note over P,C: both see the same 5 MB, marked read-only
    C-&amp;gt;&amp;gt;K: write to one pixel
    K-&amp;gt;&amp;gt;K: page fault, copy just that one 4 KB page
    K--&amp;gt;&amp;gt;C: retry the write on the private copy
    Note over P,C: every untouched page is still shared&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;So the child starts almost for free, and only pays for pages it actually changes.&lt;/p&gt;

&lt;p&gt;The picture above is the &lt;em&gt;worst case&lt;/em&gt;, where every process rewrites everything. That is the case that happens when a job really does touch the whole photo.&lt;/p&gt;

&lt;p&gt;The real difference is not "copy vs no copy" so much as &lt;strong&gt;"shared by default" vs "private by default".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that, as we are about to see, is where all the trouble comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kernel's dirty secret: they are the same thing
&lt;/h2&gt;

&lt;p&gt;Here is the sentence that rewired my head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To the Linux kernel, a process and a thread are nearly the same object.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both are just a &lt;em&gt;task&lt;/em&gt; that the scheduler puts onto a CPU.&lt;/p&gt;

&lt;p&gt;The scheduler does not schedule "processes". It schedules tasks. A four-thread process is four tasks that happen to be friends.&lt;/p&gt;

&lt;p&gt;The only real difference is memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make a new task with a &lt;strong&gt;fresh&lt;/strong&gt; slice, and you have a process&lt;/li&gt;
&lt;li&gt;make a new task pointed at an &lt;strong&gt;existing&lt;/strong&gt; slice, and you have a thread&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Linux that choice is one function: &lt;a href="https://man7.org/linux/man-pages/man2/clone.2.html" rel="noopener noreferrer"&gt;&lt;code&gt;clone()&lt;/code&gt;&lt;/a&gt;, and one flag: &lt;code&gt;CLONE_VM&lt;/code&gt;.&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%2Fm1cy8shb3fxsxzfgln90.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%2Fm1cy8shb3fxsxzfgln90.png" alt="Diagram: two columns showing clone with CLONE_VM not set giving the child task a new memory slice, and clone with CLONE_VM set pointing the child task at the parent's existing slice" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do not take my word for it. Here is a tiny program that does the same write twice, once from a child process and once from a thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;brightness&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="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pid_t&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fork&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;child&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;_exit&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="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// a process writes&lt;/span&gt;
    &lt;span class="n"&gt;waitpid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&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;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"after a child PROCESS wrote 100: brightness = %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;pthread_t&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;pthread_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// a thread writes&lt;/span&gt;
    &lt;span class="n"&gt;pthread_join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"after a THREAD wrote 100:        brightness = %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brightness&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;I compiled it and ran it on Ubuntu 24.04:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;after a child PROCESS wrote 100: brightness = 0
after a THREAD wrote 100:        brightness = 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same line of code in both. The process wrote into &lt;em&gt;its own copy&lt;/em&gt; and the parent never saw it. The thread wrote into &lt;em&gt;the&lt;/em&gt; copy.&lt;/p&gt;

&lt;p&gt;Then I ran the same binary under &lt;code&gt;strace -f -e trace=clone,clone3&lt;/code&gt; to see what glibc really asked the kernel for (trimmed a little):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD)
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|...})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first line is &lt;code&gt;fork()&lt;/code&gt;. No &lt;code&gt;CLONE_VM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second is &lt;code&gt;pthread_create()&lt;/code&gt;. There it is, &lt;code&gt;CLONE_VM&lt;/code&gt;, plus friends.&lt;/p&gt;

&lt;p&gt;Those friends decide what &lt;em&gt;else&lt;/em&gt; gets shared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CLONE_FILES&lt;/code&gt;: the open file descriptors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLONE_SIGHAND&lt;/code&gt;: the signal handlers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLONE_THREAD&lt;/code&gt;: the new task joins the same &lt;strong&gt;thread group&lt;/strong&gt;, which is why &lt;code&gt;getpid()&lt;/code&gt; returns the same number for every thread and the kernel hands each one its own thread id instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(In my run the main thread and the worker both reported the same &lt;code&gt;pid&lt;/code&gt;, and the worker had its own &lt;code&gt;tid&lt;/code&gt;. The man page for &lt;a href="https://man7.org/linux/man-pages/man2/gettid.2.html" rel="noopener noreferrer"&gt;&lt;code&gt;gettid&lt;/code&gt;&lt;/a&gt; covers the details.)&lt;/p&gt;

&lt;p&gt;And what is private to each thread? Its own &lt;strong&gt;stack&lt;/strong&gt;, its own &lt;strong&gt;registers&lt;/strong&gt;, and its own thread-local storage. Everything else, the heap and the globals and the open files, is shared.&lt;/p&gt;

&lt;p&gt;A fun side effect of all this: containers are the same trick. &lt;a href="https://man7.org/linux/man-pages/man7/namespaces.7.html" rel="noopener noreferrer"&gt;Namespaces&lt;/a&gt; are created with &lt;code&gt;CLONE_NEW*&lt;/code&gt; flags on the same &lt;code&gt;clone()&lt;/code&gt; call. Linux has one very flexible "make a task" function, and the whole zoo of processes, threads and containers is just different flag combinations.&lt;/p&gt;
&lt;h2&gt;
  
  
  Danger one: two threads, one pixel
&lt;/h2&gt;

&lt;p&gt;Now for the bill.&lt;/p&gt;

&lt;p&gt;The kernel let all four threads into the same memory. That is the whole point. It is also the whole problem.&lt;/p&gt;

&lt;p&gt;If two threads write the same pixel at the same time, you get a &lt;strong&gt;race condition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how it goes wrong. Both threads add 10 to a pixel that holds 50:&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%2F8lvqgjfpbfd86xu4cye5.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%2F8lvqgjfpbfd86xu4cye5.png" alt="Diagram: thread A and thread B both reading the pixel value 50 before either writes, each computing 60 and writing 60, so the pixel ends at 60 instead of 70" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both read 50. Both compute 60. Both write 60.&lt;/p&gt;

&lt;p&gt;You expected 70. You got 60.&lt;/p&gt;

&lt;p&gt;There is no crash. There is no error. One brighten operation simply evaporated.&lt;/p&gt;

&lt;p&gt;I wanted a real number instead of a hand-wavy one, so I ran four threads, each bumping one shared counter a million times:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;unsafe&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// shared, no protection&lt;/span&gt;
    &lt;span class="n"&gt;pthread_mutex_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// shared, behind a mutex&lt;/span&gt;
    &lt;span class="n"&gt;pthread_mutex_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lock&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected: 4000000
no lock:  3966879
mutex:    4000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Over thirty thousand increments went missing, and the program exited happily. A second run lost a different amount.&lt;/p&gt;

&lt;p&gt;That is what makes races miserable. They are not wrong in a consistent way. They are wrong in a &lt;em&gt;different&lt;/em&gt; way every time, which is a spectacular property for a bug to have when you are trying to reproduce it.&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%2Fsd3zkkq0j845lqe61pby.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%2Fsd3zkkq0j845lqe61pby.png" alt="I Immediately Regret This Decision meme: four threads, one shared counter, no mutex" width="360" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The textbook fix is a &lt;strong&gt;lock&lt;/strong&gt;. Only one thread at a time may touch the contended spot, and the rest wait their turn.&lt;/p&gt;

&lt;p&gt;That works. It also costs you, because threads that wait are threads that are not working.&lt;/p&gt;

&lt;p&gt;The better fix is to need no lock at all.&lt;/p&gt;

&lt;p&gt;Look again at the photo example. Each thread was given its &lt;em&gt;own quarter&lt;/em&gt;. Nobody writes anyone else's pixels, so nobody has to wait for anybody.&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%2F8ec3wutttsvudtxns029.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%2F8ec3wutttsvudtxns029.png" alt="Captain Hindsight meme: gave each thread its own quarter of the photo, no locks needed" width="360" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The best concurrency code is the kind where the threads rarely touch the same data. Partition first, lock only the seams.&lt;/p&gt;
&lt;h2&gt;
  
  
  Danger two: shared memory means shared fate
&lt;/h2&gt;

&lt;p&gt;The second bill is nastier because it is not subtle.&lt;/p&gt;

&lt;p&gt;All four threads live in one address space. So when one of them does something the kernel considers fatal, the kernel has no way of killing "just that thread".&lt;/p&gt;

&lt;p&gt;A segmentation fault on any thread, and the default action for that signal is to &lt;a href="https://man7.org/linux/man-pages/man7/signal.7.html" rel="noopener noreferrer"&gt;terminate the whole process&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I did not want to just claim that, so I tried it. One thread ticks along. Another sleeps half a second, then writes through a null pointer:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;steady thread: tick 1
steady thread: tick 2
steady thread: tick 3
Segmentation fault (core dumped)       # exit code 139
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The steady thread had two more ticks to go. It never got 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%2Fkkj7egon4uek87a0sd22.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%2Fkkj7egon4uek87a0sd22.png" alt="Diagram: a photo editor process with four threads, one dereferencing NULL, the kernel sending SIGSEGV and killing the entire process, while Spotify as a separate process keeps playing" width="800" height="609"&gt;&lt;/a&gt;&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%2F50kcpv2madwyzfzwerx3.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%2F50kcpv2madwyzfzwerx3.png" alt="And It's Gone meme: one thread dereferences NULL, and all four of them are gone" width="360" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three perfectly healthy threads, executed for the crime of sharing an address space with a guilty one.&lt;/p&gt;

&lt;p&gt;The same logic applies to quieter failures. A buffer overflow in one thread can scribble over memory that another thread is using, and the crash shows up somewhere completely unrelated, an hour later.&lt;/p&gt;

&lt;p&gt;Meanwhile Spotify, a separate process, is still playing. That wall from the first picture just paid for itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  So which one do I pick?
&lt;/h2&gt;

&lt;p&gt;You can see this trade-off baked into real software.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.chromium.org/developers/design-documents/multi-process-architecture/" rel="noopener noreferrer"&gt;Chrome gives each site its own process&lt;/a&gt;, so one rogue tab cannot take the browser down with it. Isolation is worth the memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/connect-estab.html" rel="noopener noreferrer"&gt;PostgreSQL uses a process per connection&lt;/a&gt;, so a crashing backend does not take the whole database with it. Isolation is worth the memory there too.&lt;/p&gt;

&lt;p&gt;On the other side, anything that crunches one big shared dataset on many cores, like our photo brightener, wants threads, because copying and stitching would eat the speedup.&lt;/p&gt;

&lt;p&gt;(And if you are in Python, the &lt;a href="https://docs.python.org/3/glossary.html#term-global-interpreter-lock" rel="noopener noreferrer"&gt;GIL&lt;/a&gt; means threads will not run your Python bytecode in parallel in the standard build, which is why so many people reach for processes there. That is a language story, not a kernel one.)&lt;/p&gt;

&lt;p&gt;Here is the decision as a flowchart:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[you need concurrency] --&amp;gt; B{must one failure&amp;lt;br/&amp;gt;stay contained?}
    B --&amp;gt;|yes: untrusted code,&amp;lt;br/&amp;gt;plugins, separate tenants| P[processes]
    B --&amp;gt;|no| C{do the workers need to&amp;lt;br/&amp;gt;share lots of data fast?}
    C --&amp;gt;|no| P2[processes,&amp;lt;br/&amp;gt;simpler and safer]
    C --&amp;gt;|yes| D{can you partition the data so&amp;lt;br/&amp;gt;threads rarely touch the same bytes?}
    D --&amp;gt;|yes| T[threads,&amp;lt;br/&amp;gt;almost no locks]
    D --&amp;gt;|no| T2[threads plus locks,&amp;lt;br/&amp;gt;and budget for the debugging]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef proc fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef thr fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a

    class B,C,D decision
    class A start
    class P,P2 proc
    class T,T2 thr&lt;/code&gt;&lt;/pre&gt;


&lt;h2&gt;
  
  
  The 30 second interview answer
&lt;/h2&gt;

&lt;p&gt;If somebody asks you the question tomorrow, here is the whole thing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The kernel gives every &lt;strong&gt;process&lt;/strong&gt; its own private memory, and the wall between processes is why one app cannot corrupt another.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;thread&lt;/strong&gt; is a task the kernel points at memory that already exists, so threads share one address space and nothing gets copied.&lt;/li&gt;
&lt;li&gt;On Linux both are made by &lt;code&gt;clone()&lt;/code&gt;. The difference is basically one flag, &lt;code&gt;CLONE_VM&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Sharing is the speed, and it is also the two dangers: &lt;strong&gt;races&lt;/strong&gt; when threads touch the same data, and &lt;strong&gt;shared fate&lt;/strong&gt; when one thread crashes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Threads are lightweight" was never wrong.&lt;/p&gt;

&lt;p&gt;It just left out what you are paying with.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Risk-Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is built for engineering teams that ship often, but also care about keeping production stable for customers. It approves some 40-50% of low-risk changes automatically, and it gives your team the tools to handle the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand risky changes faster.&lt;/li&gt;
&lt;li&gt;Fix problems in those changes, with human attention directed to the work that needs it.&lt;/li&gt;
&lt;li&gt;Enforce your engineering standards.&lt;/li&gt;
&lt;li&gt;Learn from production outcomes, such as incidents, rollbacks, and queue wait times.&lt;/li&gt;
&lt;li&gt;Improve the review process over time and make it more efficient.&lt;/li&gt;
&lt;li&gt;Connect to the major engineering tools that your team already uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Auto-Approve: Ship the Safe 50% Without a Human&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Auto-Approve Upto 50% of Your Pull Requests, Safely.&lt;/strong&gt; Risk scoring decides which MRs/PRs are safe enough to ship without a human, so your team only spends review time on risky and critical changes.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;demo.mp4&lt;/span&gt;
    
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview auto-approving a low-risk change while&lt;/i&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>operatingsystems</category>
      <category>concurrency</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Do You Make a Microservice Only Your Other Services Can Call?</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:50:30 +0000</pubDate>
      <link>https://dev.to/lovestaco/how-do-you-make-a-microservice-only-your-other-services-can-call-2f1f</link>
      <guid>https://dev.to/lovestaco/how-do-you-make-a-microservice-only-your-other-services-can-call-2f1f</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is a classic microservices interview question.&lt;/p&gt;

&lt;p&gt;"One of your services should only accept calls from other internal services. How do you secure it?"&lt;/p&gt;

&lt;p&gt;Most people answer in one line.&lt;/p&gt;

&lt;p&gt;"Put it in a private network, no public IP."&lt;/p&gt;

&lt;p&gt;That answer is correct.&lt;/p&gt;

&lt;p&gt;It is also about 25% of the answer, and the other 75% is where the interview is actually won.&lt;/p&gt;

&lt;p&gt;So let's take the question apart the way an attacker would, one layer at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: a payments service with a secret
&lt;/h2&gt;

&lt;p&gt;Make it concrete.&lt;/p&gt;

&lt;p&gt;A user places an order. The order service calls the payments service to charge the card.&lt;/p&gt;

&lt;p&gt;That is the &lt;strong&gt;only&lt;/strong&gt; legitimate way payments is ever used.&lt;/p&gt;

&lt;p&gt;No user should talk to it directly. Nothing on the internet should even know it exists.&lt;/p&gt;

&lt;p&gt;So the goal is not "make payments secure".&lt;/p&gt;

&lt;p&gt;The goal is narrower and much more useful: &lt;strong&gt;make payments answer exactly one caller, and prove who that caller is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hold on to that sentence, because every layer below is an attempt to make it true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: no public address
&lt;/h2&gt;

&lt;p&gt;Start with the thing everyone says.&lt;/p&gt;

&lt;p&gt;In Kubernetes, a &lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/" rel="noopener noreferrer"&gt;Service&lt;/a&gt; defaults to type &lt;code&gt;ClusterIP&lt;/code&gt;, which gets a virtual IP that is only routable inside the cluster.&lt;/p&gt;

&lt;p&gt;So you do not give payments a &lt;code&gt;LoadBalancer&lt;/code&gt; or a &lt;code&gt;NodePort&lt;/code&gt;, and you make sure no Ingress rule points at it.&lt;/p&gt;

&lt;p&gt;From the outside, the service simply does not exist. There is nothing to scan and nothing to brute force.&lt;/p&gt;

&lt;p&gt;That is a genuinely good first move, and it kills the laziest class of attack, which is "somebody found the URL".&lt;/p&gt;

&lt;p&gt;Now look at what it does not do.&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%2F404rhu9auyrdqs7zmc19.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%2F404rhu9auyrdqs7zmc19.png" alt="Diagram: the internet blocked at a dashed wall by a ClusterIP, while inside the cluster a compromised marketing site and the order service can both reach payments, with the note that a private network keeps strangers out, not tenants" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside the cluster, the network is &lt;strong&gt;flat by default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every pod can open a connection to every other pod, and there is no door between them.&lt;/p&gt;

&lt;p&gt;Your marketing website runs in the same cluster. It has a CMS, three plugins nobody updated, and a contact form written during a hackathon.&lt;/p&gt;

&lt;p&gt;An attacker finds one bug in it.&lt;/p&gt;

&lt;p&gt;Congratulations, they are now &lt;em&gt;inside your private network&lt;/em&gt;, and payments will happily answer them, because from payments' point of view the request came from a neighbour.&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%2Fzzbeu4xu1ddg81tclpwy.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%2Fzzbeu4xu1ddg81tclpwy.png" alt="I Am The Captain Now meme: payments sits in a private network with no public IP, and a compromised marketing pod says it is the captain now" width="360" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Private" is a statement about &lt;strong&gt;strangers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It says nothing about &lt;strong&gt;tenants&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: a firewall for pods
&lt;/h2&gt;

&lt;p&gt;The fix for a flat network is to stop it being flat.&lt;/p&gt;

&lt;p&gt;Kubernetes has &lt;a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/" rel="noopener noreferrer"&gt;NetworkPolicy&lt;/a&gt; for exactly this. Think of it as a firewall rule that follows pods around.&lt;/p&gt;

&lt;p&gt;The pattern is two policies. First close everything, then name exactly who gets in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. the namespace starts closed&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default-deny-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Ingress&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# 2. payments accepts order pods, on its own port, and nothing else&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-from-order&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;payments&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Ingress&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;order&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the compromised marketing pod can still open a socket, but its packets are dropped on the way in.&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%2F6mwykv6xssopvbu4yomk.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%2F6mwykv6xssopvbu4yomk.png" alt="Diagram: an order pod passing through a NetworkPolicy wall to payments while a marketing pod is dropped, and a red panel listing what a network policy cannot see" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One trap here, and it is a nasty one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A NetworkPolicy is only enforced if your network plugin enforces it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Kubernetes docs are blunt about this: if the plugin does not implement policies, creating the object &lt;a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/" rel="noopener noreferrer"&gt;has no effect&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Calico and Cilium enforce them. Some setups do not, or need a switch flipped on.&lt;/p&gt;

&lt;p&gt;So you can write a lovely allow list, &lt;code&gt;kubectl apply&lt;/code&gt; it, get a calm green "created", and be protected by absolutely nothing.&lt;/p&gt;

&lt;p&gt;Test that it actually drops traffic. We will do that at the end.&lt;/p&gt;
&lt;h3&gt;
  
  
  The blind spot
&lt;/h3&gt;

&lt;p&gt;Even when it is enforced, a network policy has a fundamental limit.&lt;/p&gt;

&lt;p&gt;It decides based on &lt;strong&gt;where a packet came from&lt;/strong&gt;, not &lt;strong&gt;who actually sent it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is checking the address on the envelope, and says nothing about the person who wrote the letter.&lt;/p&gt;

&lt;p&gt;In practice that breaks down in three ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The label is the identity, and labels are cheap.&lt;/strong&gt;&lt;br&gt;
Anyone who can create a pod with &lt;code&gt;app: order&lt;/code&gt; in that namespace walks straight through the allow list. A policy trusts the label, and nothing verifies the label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is blind above the network layer.&lt;/strong&gt;&lt;br&gt;
It knows "TCP, port 8080". It has no idea whether the request is &lt;code&gt;POST /charge&lt;/code&gt; or &lt;code&gt;DELETE /everything&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not encrypt anything.&lt;/strong&gt;&lt;br&gt;
Inside the cluster, the bytes are readable by anyone who can sniff the node's traffic.&lt;/p&gt;

&lt;p&gt;Pod IPs also get recycled constantly, so an address that was the order service a minute ago can belong to a different pod now.&lt;/p&gt;

&lt;p&gt;That is why a good policy matches on labels instead of raw IPs.&lt;/p&gt;

&lt;p&gt;But the deeper problem is the one above: there is no proof attached to the claim.&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%2F69c82itrxwmf83bq4sof.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%2F69c82itrxwmf83bq4sof.png" alt="Elf meme: trusting a pod's IP address in Kubernetes, you sit on a throne of lies" width="360" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A network policy is great at shrinking the blast radius.&lt;/p&gt;

&lt;p&gt;It is not great at proving identity.&lt;/p&gt;

&lt;p&gt;For that, you need ID cards.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 3: mutual TLS
&lt;/h2&gt;

&lt;p&gt;Regular TLS is what your browser does. The &lt;strong&gt;server&lt;/strong&gt; shows a certificate, and you check it.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;mutual&lt;/strong&gt; TLS, the client shows one too, and the server checks it before any data moves.&lt;/p&gt;

&lt;p&gt;So when order calls payments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order proves "I am the order service"&lt;/li&gt;
&lt;li&gt;payments proves "I am the payments service"&lt;/li&gt;
&lt;li&gt;both are verified against the same trusted authority, and only then does the request go through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An attacker sitting inside the network can reach the port. They cannot &lt;em&gt;forge a certificate&lt;/em&gt; signed by your authority, so the handshake fails and no request is ever read.&lt;/p&gt;

&lt;p&gt;That one change moves you from "trust the address" to "trust the proof".&lt;/p&gt;
&lt;h3&gt;
  
  
  Who hands out the certificates?
&lt;/h3&gt;

&lt;p&gt;This is the practical question that makes most people wave their hands.&lt;/p&gt;

&lt;p&gt;Every service needs its own certificate, issued by &lt;strong&gt;one authority that you run inside the cluster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The identity inside it is usually a &lt;a href="https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/" rel="noopener noreferrer"&gt;SPIFFE&lt;/a&gt; style name, and in a mesh it is derived from the pod's Kubernetes &lt;strong&gt;ServiceAccount&lt;/strong&gt;, something like &lt;code&gt;spiffe://cluster.local/ns/shop/sa/order&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two details matter more than they look:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The certificates are short-lived.&lt;/strong&gt;&lt;br&gt;
Hours, not years. A stolen cert is a very temporary problem, and nobody has a spreadsheet of expiry dates to forget about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The identity is the ServiceAccount.&lt;/strong&gt;&lt;br&gt;
If every deployment in your cluster runs as the &lt;code&gt;default&lt;/code&gt; ServiceAccount, they all share one identity, and your fancy mTLS can no longer tell them apart. Give each service its own.&lt;/p&gt;
&lt;h3&gt;
  
  
  You do not write any of this
&lt;/h3&gt;

&lt;p&gt;Nobody hand-rolls certificate rotation inside every service.&lt;/p&gt;

&lt;p&gt;You use a service mesh, such as &lt;a href="https://istio.io/latest/docs/concepts/security/" rel="noopener noreferrer"&gt;Istio&lt;/a&gt; or &lt;a href="https://linkerd.io/2-edge/features/automatic-mtls/" rel="noopener noreferrer"&gt;Linkerd&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The mesh injects a small proxy, a sidecar, next to every pod. That proxy fetches the certificate, presents it, verifies the peer's, rotates it before it expires, and encrypts the traffic.&lt;/p&gt;

&lt;p&gt;Your application keeps speaking plain HTTP to &lt;code&gt;localhost&lt;/code&gt;. It never sees a certificate.&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%2Frclaumb4m1u9ctvjrmnr.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%2Frclaumb4m1u9ctvjrmnr.png" alt="Diagram: a cluster CA issuing short-lived certs to the sidecars of an order pod and a payments pod, the two sidecars exchanging identities over an encrypted channel, and a marketing pod with no cert being refused" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Sidecars are not the only shape. Istio also has an &lt;a href="https://istio.io/latest/docs/ambient/overview/" rel="noopener noreferrer"&gt;ambient mode&lt;/a&gt; that moves the proxying into a per-node component, so the idea survives, the extra container goes away.)&lt;/p&gt;

&lt;p&gt;Here is one call, start to finish, as the time-ordered version of that picture:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant CA as Cluster CA
    participant OP as order sidecar
    participant PP as payments sidecar
    participant P as payments app

    OP-&amp;gt;&amp;gt;CA: prove who I am (ServiceAccount token)
    CA--&amp;gt;&amp;gt;OP: short-lived cert for sa/order
    PP-&amp;gt;&amp;gt;CA: prove who I am (ServiceAccount token)
    CA--&amp;gt;&amp;gt;PP: short-lived cert for sa/payments
    Note over OP,PP: later, the order app makes a call
    OP-&amp;gt;&amp;gt;PP: TLS handshake, here is my cert
    PP--&amp;gt;&amp;gt;OP: and here is mine
    Note over OP,PP: both verify against the CA root
    OP-&amp;gt;&amp;gt;PP: POST /charge, encrypted
    PP-&amp;gt;&amp;gt;P: plain request on localhost
    P--&amp;gt;&amp;gt;OP: 200, back through both proxies&lt;/code&gt;&lt;/pre&gt;


&lt;h3&gt;
  
  
  The gotcha that gets teams
&lt;/h3&gt;

&lt;p&gt;Installing a mesh does not mean you are enforcing mTLS.&lt;/p&gt;

&lt;p&gt;Istio's default mode is &lt;a href="https://istio.io/latest/docs/tasks/security/authentication/mtls-migration/" rel="noopener noreferrer"&gt;PERMISSIVE&lt;/a&gt;, which accepts &lt;strong&gt;both&lt;/strong&gt; mTLS and plain text, so that you can migrate without breaking things.&lt;/p&gt;

&lt;p&gt;That is a sensible default for rollout.&lt;/p&gt;

&lt;p&gt;It is also a state in which an attacker's plain &lt;code&gt;curl&lt;/code&gt; still works, while your dashboard cheerfully says "mesh enabled".&lt;/p&gt;

&lt;p&gt;You have to flip payments to &lt;code&gt;STRICT&lt;/code&gt; on purpose. The config for that is in the next section.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 4: authenticated is not the same as allowed
&lt;/h2&gt;

&lt;p&gt;Now every caller has a verified identity.&lt;/p&gt;

&lt;p&gt;So the next question is the one most people forget: &lt;strong&gt;should this caller be allowed to do this thing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The order service may call payments.&lt;/p&gt;

&lt;p&gt;It has no business calling the refund service, because refunds belong to the support service.&lt;/p&gt;

&lt;p&gt;Here is why this matters. Say the marketing site also runs in the mesh. It has a sidecar, so it has a perfectly valid certificate.&lt;/p&gt;

&lt;p&gt;mTLS says: "yes, that really is the marketing service."&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; say: "and the marketing service may charge cards."&lt;/p&gt;

&lt;p&gt;That second sentence is an authorization policy, and the same mesh enforces it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PeerAuthentication&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mtls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STRICT&lt;/span&gt;          &lt;span class="c1"&gt;# the default is PERMISSIVE, which still accepts plaintext&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthorizationPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-allow-order&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ALLOW&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;principals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster.local/ns/shop/sa/order"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/charge"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Read the &lt;code&gt;rules&lt;/code&gt; block out loud.&lt;/p&gt;

&lt;p&gt;"The identity &lt;code&gt;sa/order&lt;/code&gt; may &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/charge&lt;/code&gt; on payments."&lt;/p&gt;

&lt;p&gt;Once an &lt;code&gt;ALLOW&lt;/code&gt; policy exists for a workload, &lt;a href="https://istio.io/latest/docs/reference/config/security/authorization-policy/" rel="noopener noreferrer"&gt;anything that does not match a rule is denied&lt;/a&gt;. You get default-deny for free, at the level of &lt;strong&gt;identity and URL&lt;/strong&gt; rather than IP and port.&lt;/p&gt;

&lt;p&gt;The proxy next to payments checks this on every single request.&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%2Fwkhpw2369u6r7x6651l9.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%2Fwkhpw2369u6r7x6651l9.png" alt="Diagram: order service allowed to POST /charge on payments, support service allowed to POST /refunds on refunds, and the order service denied with a 403 when it tries to reach refunds" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is least privilege, applied between services instead of between people.&lt;/p&gt;

&lt;p&gt;If someone steals the order service's identity, they can charge cards. They still cannot issue refunds, because that was never in its rules.&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%2F0gahg39qc3p2tgp1vby6.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%2F0gahg39qc3p2tgp1vby6.png" alt="We Don't Do That Here meme: the order service calling the refund API" width="360" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Linkerd has the same idea under different names, a &lt;code&gt;Server&lt;/code&gt; plus an &lt;code&gt;AuthorizationPolicy&lt;/code&gt; that &lt;a href="https://linkerd.io/2-edge/features/server-policy/" rel="noopener noreferrer"&gt;binds identities to routes&lt;/a&gt;. The shape transfers, the YAML does not.&lt;/p&gt;
&lt;h2&gt;
  
  
  Proving it, because "created" is not "enforced"
&lt;/h2&gt;

&lt;p&gt;Every one of these layers fails differently, and that is a feature.&lt;/p&gt;

&lt;p&gt;Run one request from a pod you are not supposed to trust, and the &lt;strong&gt;way it fails&lt;/strong&gt; tells you which gate caught it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/marketing &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 3 &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://payments.shop:8080/charge

&lt;span class="c"&gt;# NetworkPolicy dropped it   -&amp;gt; curl: (28) Connection timed out after 3001 milliseconds&lt;/span&gt;
&lt;span class="c"&gt;# mTLS STRICT refused it     -&amp;gt; curl: (56) Recv failure: Connection reset by peer&lt;/span&gt;
&lt;span class="c"&gt;# AuthorizationPolicy said no -&amp;gt; RBAC: access denied   (HTTP 403)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Those are the usual signatures. The exact text shifts a little between versions and tools, but the &lt;em&gt;character&lt;/em&gt; is stable: a hang, a reset, a polite 403.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[a call to payments fails] --&amp;gt; B{what does the&amp;lt;br/&amp;gt;client actually see?}
    B --&amp;gt;|name does not resolve,&amp;lt;br/&amp;gt;or no route at all| C[ClusterIP: you are outside the cluster]
    B --&amp;gt;|hangs, then times out| D[NetworkPolicy dropped the packets]
    B --&amp;gt;|connection reset| E[mTLS: no valid cert,&amp;lt;br/&amp;gt;or plaintext sent to a STRICT peer]
    B --&amp;gt;|HTTP 403 RBAC access denied| F[AuthorizationPolicy: valid identity,&amp;lt;br/&amp;gt;not allowed to do this]
    B --&amp;gt;|HTTP 200| G[all four gates said yes]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef block fill:#ff6b6b,stroke:#c94a4a,color:#1a1a1a
    classDef ok fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a

    class B decision
    class A start
    class C,D,E,F block
    class G ok&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;One honest caveat on that chart.&lt;/p&gt;

&lt;p&gt;A timeout is ambiguous. "No route" and "dropped by policy" can both look like a hang from the client side.&lt;/p&gt;

&lt;p&gt;That is why you test from &lt;strong&gt;inside&lt;/strong&gt; a pod that should be blocked, and from a pod that should be allowed. Both directions. If the blocked one succeeds, your network plugin is not enforcing anything.&lt;/p&gt;

&lt;p&gt;Also note the marketing pod, if it is in the mesh, will sail past gate three and be stopped at gate four. A request that dies at the last gate is not a failure of the earlier ones, it is the reason the last one exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;Here are the four gates in one picture:&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%2Frq0f00v0j0rc07lzk7s2.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%2Frq0f00v0j0rc07lzk7s2.png" alt="Diagram: an attacker facing four gates in a row before reaching payments, ClusterIP, NetworkPolicy, mutual TLS and authorization, each labeled with what it stops and what the failure looks like" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each one stops a &lt;strong&gt;different mistake&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClusterIP&lt;/strong&gt; stops the internet. It sees: no route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NetworkPolicy&lt;/strong&gt; stops other pods. It sees: a timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mTLS&lt;/strong&gt; stops pods with no identity. It sees: a reset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AuthorizationPolicy&lt;/strong&gt; stops the wrong service. It sees: a 403.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The attacker has to beat all four.&lt;/p&gt;

&lt;p&gt;You only need one of them to hold.&lt;/p&gt;

&lt;p&gt;That is the actual interview answer: &lt;strong&gt;defense in depth&lt;/strong&gt;, where no single layer has to be perfect, because each is covering for a hole in the one before it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is still left (the part that gets you the senior grade)
&lt;/h2&gt;

&lt;p&gt;Four gates is a strong answer. It is not the end of the story, and it helps to say so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mTLS authenticates services, not users.&lt;/strong&gt;&lt;br&gt;
If the order service is compromised, or has an SSRF bug, it can ask payments to charge any card, and every gate above will wave it through, because it really is the order service.&lt;/p&gt;

&lt;p&gt;This is the classic &lt;a href="https://en.wikipedia.org/wiki/Confused_deputy_problem" rel="noopener noreferrer"&gt;confused deputy&lt;/a&gt; problem.&lt;/p&gt;

&lt;p&gt;The usual answer is to carry the end user's identity downstream, for example a signed token that payments validates with something like Istio's &lt;a href="https://istio.io/latest/docs/reference/config/security/request_authentication/" rel="noopener noreferrer"&gt;RequestAuthentication&lt;/a&gt;, plus sanity checks such as "does this order id belong to this user".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stolen credentials still work until they expire.&lt;/strong&gt;&lt;br&gt;
Short-lived certs shrink that window. They do not remove it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can do all of this without a mesh.&lt;/strong&gt;&lt;br&gt;
Cloud providers have their own service-to-service identity, such as IAM based auth on AWS or Google Cloud. SPIFFE and its runtime, SPIRE, work outside Kubernetes. A mesh is the convenient way to get the same ideas, it is not the only way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mesh is now critical infrastructure.&lt;/strong&gt;&lt;br&gt;
If your certificate authority goes down for long enough, certs expire and services stop trusting each other. Monitor it like you monitor your database.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 30 second version
&lt;/h2&gt;

&lt;p&gt;If you get asked this in an interview, here is the whole thing in one breath:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give the service a &lt;code&gt;ClusterIP&lt;/code&gt; and no public address, so the internet cannot reach it.&lt;/li&gt;
&lt;li&gt;Add a default-deny &lt;code&gt;NetworkPolicy&lt;/code&gt; with an allow list, so only the right pods can connect.&lt;/li&gt;
&lt;li&gt;Turn on mutual TLS, in &lt;code&gt;STRICT&lt;/code&gt; mode, so callers prove who they are with short-lived certificates.&lt;/li&gt;
&lt;li&gt;Add an authorization policy, so even a verified caller can only do what it is supposed to do.&lt;/li&gt;
&lt;li&gt;Then mention that none of this authenticates the end user, and you will have said something most candidates do not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Put it in a private network" was never wrong.&lt;/p&gt;

&lt;p&gt;It was just the first gate of four.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Risk-Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is built for engineering teams that ship often, but also care about keeping production stable for customers. It approves some 40-50% of low-risk changes automatically, and it gives your team the tools to handle the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand risky changes faster.&lt;/li&gt;
&lt;li&gt;Fix problems in those changes, with human attention directed to the work that needs it.&lt;/li&gt;
&lt;li&gt;Enforce your engineering standards.&lt;/li&gt;
&lt;li&gt;Learn from production outcomes, such as incidents, rollbacks, and queue wait times.&lt;/li&gt;
&lt;li&gt;Improve the review process over time and make it more efficient.&lt;/li&gt;
&lt;li&gt;Connect to the major engineering tools that your team already uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Auto-Approve: Ship the Safe 50% Without a Human&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Auto-Approve Upto 50% of Your Pull Requests, Safely.&lt;/strong&gt; Risk scoring decides which MRs/PRs are safe enough to ship without a human, so your team only spends review time on risky and critical changes.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;demo.mp4&lt;/span&gt;
    
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview auto-approving a low-risk change while&lt;/i&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>microservices</category>
      <category>devops</category>
    </item>
    <item>
      <title>One Rust Unicode Bug, Three Coding Models: A Reproducible Comparison</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Sun, 18 Oct 2026 05:10:27 +0000</pubDate>
      <link>https://dev.to/lovestaco/one-rust-unicode-bug-three-coding-models-a-reproducible-comparison-48ae</link>
      <guid>https://dev.to/lovestaco/one-rust-unicode-bug-three-coding-models-a-reproducible-comparison-48ae</guid>
      <description>&lt;p&gt;Coding-model leaderboards reveal little about how a model handles a real bug. A Rust function can work on ASCII and still panic on valid Unicode because it slices UTF-8 at the wrong byte boundary. The fix must respect character boundaries and preserve the &lt;code&gt;&amp;amp;str&lt;/code&gt; return type. This article uses one reproducible bug, one prompt, and the same regression tests to compare three models on the quality of their patches, not the confidence of their explanations.&lt;/p&gt;

&lt;p&gt;A one-line Rust slice can panic on valid UTF-8. That makes it a useful coding-model challenge. The fix must preserve the function signature, handle character boundaries, and pass regression tests. This comparison uses the same bug for three models and judges their patches using compiler output rather than leaderboard scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;The function below is supposed to return the first &lt;code&gt;n&lt;/code&gt; characters of a string. It works on plain ASCII and panics when &lt;code&gt;n&lt;/code&gt; cuts through a multibyte UTF-8 character.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.len&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;input&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;amp;&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[cfg(test)]&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rustacean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"rust"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;unicode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"café"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"café"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;beyond_length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"é"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&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;The original implementation counts bytes, not characters. In UTF-8, the final &lt;code&gt;é&lt;/code&gt; in &lt;code&gt;café&lt;/code&gt; occupies two bytes. Slicing at byte index four cuts through that character and panics. Save the code as &lt;code&gt;src/lib.rs&lt;/code&gt; in a new Cargo library project. Run &lt;code&gt;cargo test&lt;/code&gt; to reproduce the failing Unicode case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three models, one task
&lt;/h2&gt;

&lt;p&gt;The three candidates are &lt;a href="https://aimlapi.com/models/deepseek-v4-pro" rel="noopener noreferrer"&gt;DeepSeek V4 Pro&lt;/a&gt;, &lt;a href="https://aimlapi.com/models/claude-fable-5-1" rel="noopener noreferrer"&gt;Claude Fable 5.1&lt;/a&gt;, and &lt;a href="https://aimlapi.com/models/grok-build-0-1" rel="noopener noreferrer"&gt;Grok Build 0.1&lt;/a&gt;. These are the target model pages for the comparison. Model availability, exact API identifiers, and supported parameters must be checked against the live catalog before execution.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;API identifier&lt;/th&gt;
&lt;th&gt;Temperature&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;td&gt;Confirm from catalog&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same Rust file and prompt&lt;/td&gt;
&lt;td&gt;Record API version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5.1&lt;/td&gt;
&lt;td&gt;Confirm from catalog&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same Rust file and prompt&lt;/td&gt;
&lt;td&gt;Record API version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok Build 0.1&lt;/td&gt;
&lt;td&gt;Confirm from catalog&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same Rust file and prompt&lt;/td&gt;
&lt;td&gt;Record API version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the same request body, system instructions, maximum output allowance, and test environment for every model. Record the exact model ID returned by the API. A zero temperature reduces randomness but does not guarantee identical answers on repeated calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;Use the following prompt unchanged for every model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fix the Rust function below. It must return the first n Unicode scalar values without panicking. Keep the return type &amp;amp;str. Explain the bug briefly, return a complete src/lib.rs, and add a regression test for an emoji and a combining character. Do not add external dependencies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Append the entire original &lt;code&gt;src/lib.rs&lt;/code&gt; after that instruction. Do not tell one model about another model's failure. Do not silently repair a response before testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to inspect in each model's patch
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DeepSeek V4 Pro
&lt;/h3&gt;

&lt;p&gt;Capture the raw response, its reported input and output tokens, the elapsed request time, and the first &lt;code&gt;cargo test&lt;/code&gt; output. A correct patch should iterate over &lt;code&gt;char&lt;/code&gt; boundaries rather than slice at an arbitrary byte index. If the returned code does not compile, retain the error and count the next prompt as another attempt. Add a screenshot of the response and terminal output from the actual run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Fable 5.1
&lt;/h3&gt;

&lt;p&gt;Use the identical source and prompt. Record whether the model preserves the borrowed &lt;code&gt;&amp;amp;str&lt;/code&gt; return type and whether it handles a request longer than the character count. Check the combining-character case carefully. Unicode scalar values are not the same as grapheme clusters. A request for one scalar value from an &lt;code&gt;e&lt;/code&gt; followed by a combining accent may return only the &lt;code&gt;e&lt;/code&gt;. That is correct under this task's definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grok Build 0.1
&lt;/h3&gt;

&lt;p&gt;Repeat the procedure without editing the prompt. Review whether the model changes the API, introduces a dependency, or returns a &lt;code&gt;String&lt;/code&gt; instead of &lt;code&gt;&amp;amp;str&lt;/code&gt;. These changes matter even when the visible output looks correct. Keep the first attempt and any follow-up attempts as separate records.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a correct fix looks like
&lt;/h2&gt;

&lt;p&gt;One valid approach uses &lt;code&gt;char_indices&lt;/code&gt; to find the byte boundary after the requested number of scalar values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
        &lt;span class="nf"&gt;.char_indices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.nth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;end&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;This reference implementation is a correctness oracle for the exercise, not an attributed output from any model. The test suite should include ASCII, multibyte characters, empty strings, &lt;code&gt;n&lt;/code&gt; equal to zero, &lt;code&gt;n&lt;/code&gt; greater than the character count, emoji, and decomposed accents. The reference code should be kept out of model prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison scorecard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;First attempt passes&lt;/th&gt;
&lt;th&gt;Attempts&lt;/th&gt;
&lt;th&gt;Wall time&lt;/th&gt;
&lt;th&gt;Input tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;td&gt;Enter from cargo test&lt;/td&gt;
&lt;td&gt;Enter from logs&lt;/td&gt;
&lt;td&gt;Enter from logs&lt;/td&gt;
&lt;td&gt;Enter from API&lt;/td&gt;
&lt;td&gt;Enter from API&lt;/td&gt;
&lt;td&gt;Compiler and behavior notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5.1&lt;/td&gt;
&lt;td&gt;Enter from cargo test&lt;/td&gt;
&lt;td&gt;Enter from logs&lt;/td&gt;
&lt;td&gt;Enter from logs&lt;/td&gt;
&lt;td&gt;Enter from API&lt;/td&gt;
&lt;td&gt;Enter from API&lt;/td&gt;
&lt;td&gt;Compiler and behavior notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok Build 0.1&lt;/td&gt;
&lt;td&gt;Enter from cargo test&lt;/td&gt;
&lt;td&gt;Enter from logs&lt;/td&gt;
&lt;td&gt;Enter from logs&lt;/td&gt;
&lt;td&gt;Enter from API&lt;/td&gt;
&lt;td&gt;Enter from API&lt;/td&gt;
&lt;td&gt;Compiler and behavior notes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scorecard becomes useful once each row is populated from the first response, Cargo output, and API logs. Until then, it is a repeatable evaluation worksheet rather than a model ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to pick for each coding task
&lt;/h2&gt;

&lt;p&gt;For short patches, first-pass compilation and correct edge cases matter more than a polished explanation. For larger refactors, examine how often a model changes unrelated code. For code review, test whether it identifies the byte-versus-character distinction without prompting. For terminal automation, compare how predictably each model follows the required output format.&lt;/p&gt;

&lt;p&gt;The best LLM for coding is task dependent. One Rust bug cannot establish which model performs best across a whole repository, language, or toolchain. It can reveal whether a model handles a concrete failure mode under controlled conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to repeat the comparison
&lt;/h2&gt;

&lt;p&gt;Create a Cargo library with &lt;code&gt;cargo new unicode-prefix --lib&lt;/code&gt;. Replace &lt;code&gt;src/lib.rs&lt;/code&gt; with the buggy example and run &lt;code&gt;cargo test&lt;/code&gt;. Send the same prompt and file to each confirmed model identifier. Save the raw JSON response, request timestamp, token usage fields, and model version. Extract the proposed code into a clean working tree for each attempt. Run &lt;code&gt;cargo fmt --check&lt;/code&gt;, &lt;code&gt;cargo test&lt;/code&gt;, and &lt;code&gt;cargo clippy&lt;/code&gt;. Record failures without modifying the first answer.&lt;/p&gt;

&lt;p&gt;Keep the repository organized as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;baseline/src/lib.rs
oracle/src/lib.rs
prompts/task.txt
runs/&amp;lt;model&amp;gt;/response.json
runs/&amp;lt;model&amp;gt;/cargo-test.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publish a repository link only after the files are uploaded. The reproducible artifact is the code and logs, not a ranking graphic.&lt;/p&gt;

&lt;p&gt;A shareable comparison includes the repository, unedited first responses, and Cargo logs. Readers can inspect both the proposed fixes and the failures.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>We Rendered a HQ Demo Video on a Free Farm Run by Strangers</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Wed, 14 Oct 2026 11:08:09 +0000</pubDate>
      <link>https://dev.to/lovestaco/we-rendered-a-hq-demo-video-on-a-free-farm-run-by-strangers-1440</link>
      <guid>https://dev.to/lovestaco/we-rendered-a-hq-demo-video-on-a-free-farm-run-by-strangers-1440</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last post, my four-laptop render farm lost a race against my own laptop.&lt;/p&gt;

&lt;p&gt;I promised you the ending.&lt;/p&gt;

&lt;p&gt;Here it is.&lt;/p&gt;

&lt;p&gt;It involves a free farm run by strangers and a Java file called &lt;code&gt;sheepit-autoupdate.jar&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson I was too slow to learn
&lt;/h2&gt;

&lt;p&gt;If you missed it, &lt;a href="https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32"&gt;last time&lt;/a&gt; we turned four laptops into a Flamenco render farm.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32" class="crayons-story__hidden-navigation-link"&gt;We Built a Blender Render Farm Out of Four Laptops&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/lovestaco" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" alt="lovestaco profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/lovestaco" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Athreya aka Maneshwar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Athreya aka Maneshwar
                
                
              
              &lt;div id="story-author-preview-content-4839352" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/lovestaco" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Athreya aka Maneshwar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 12&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32" id="article-link-4839352"&gt;
          We Built a Blender Render Farm Out of Four Laptops
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/blender"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;blender&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/networking"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;networking&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tailscale"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tailscale&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;30&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            16 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


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

&lt;p&gt;And then the farm spent almost all of its time downloading a 115 MB .blend over my 5 Mbit/s home upload, while my laptop finished the job alone.&lt;/p&gt;

&lt;p&gt;The takeaway was one sentence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A render farm is a file distribution problem wearing a GPU costume.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the question for round two was not "where do I find more GPUs".&lt;/p&gt;

&lt;p&gt;It was "who can I hand a file to once, and have it spread out without my upload being the highway".&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the flock
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.sheepit-renderfarm.com/" rel="noopener noreferrer"&gt;SheepIt&lt;/a&gt; is a free, community-powered render farm for Blender.&lt;/p&gt;

&lt;p&gt;The deal is beautifully simple.&lt;/p&gt;

&lt;p&gt;You install a small client on your machine.&lt;/p&gt;

&lt;p&gt;While your machine is idle, it renders other people's frames.&lt;/p&gt;

&lt;p&gt;That earns you points.&lt;/p&gt;

&lt;p&gt;You spend the points to have the community render yours.&lt;/p&gt;

&lt;p&gt;Everyone lends a machine, everyone gets frames.&lt;/p&gt;

&lt;p&gt;I did not build a farm.&lt;/p&gt;

&lt;p&gt;I joined a flock.&lt;/p&gt;

&lt;p&gt;The client is a single Java jar (&lt;code&gt;sheepit-autoupdate.jar&lt;/code&gt;, which updates itself, hence the name), and the upload side is a web page where you drop a zip.&lt;/p&gt;

&lt;p&gt;That is the entire infrastructure I had to run.&lt;/p&gt;

&lt;p&gt;No Samba, no tailnet, no NAT archaeology.&lt;/p&gt;

&lt;p&gt;The file goes to one place, and it is their problem to hand it to a hundred machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points: the currency of the flock
&lt;/h2&gt;

&lt;p&gt;SheepIt runs on points.&lt;/p&gt;

&lt;p&gt;You earn them by rendering other people's frames.&lt;/p&gt;

&lt;p&gt;You spend them when you order your own.&lt;/p&gt;

&lt;p&gt;The core loop needs no money, just render time in and render time out.&lt;/p&gt;

&lt;p&gt;I registered on October 10th, the same Saturday the laptop farm gave up, and left the client running.&lt;/p&gt;

&lt;p&gt;Here is our profile a few days later:&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%2Fk0e48zj602x9dh7uw4y7.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%2Fk0e48zj602x9dh7uw4y7.png" alt="SheepIt profile page showing 17 projects created, 18,651 frames ordered, 6,981 frames rendered, 2 days 7 hours rendered, rank 416th, 1,261,589 points and 4 consecutive render days"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me read that back to you.&lt;/p&gt;

&lt;p&gt;Our laptops spent &lt;strong&gt;2 days and 7 hours&lt;/strong&gt; rendering strangers' frames.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;6,981 frames&lt;/strong&gt; for other people.&lt;/p&gt;

&lt;p&gt;And in return I ordered &lt;strong&gt;18,651 frames&lt;/strong&gt; across 17 projects.&lt;/p&gt;

&lt;p&gt;I ordered almost three times as many frames as I rendered, and the balance still says &lt;strong&gt;1,261,589 points&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I cannot give you SheepIt's exact exchange rate, so treat this as an observation, not a formula.&lt;/p&gt;

&lt;p&gt;As far as I can tell, a light 360p frame is cheap to order, a heavy 1080p ray traced frame costs more, and a stronger GPU earns faster.&lt;/p&gt;

&lt;p&gt;What I can show you is the shape of it:&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%2Fk0x9vhnwuubchxpy3ffy.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%2Fk0x9vhnwuubchxpy3ffy.png" alt="Two line charts from the SheepIt profile: points rising from about 270,000 on October 11th to about 1.2 million on October 14th, and frames rendered rising from about 1,400 to about 6,400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The points line only goes up.&lt;/p&gt;

&lt;p&gt;Four days of render streaks, and I never ran dry.&lt;/p&gt;

&lt;p&gt;The frames line flattens after the first couple of days, which is my guess for the laptop getting busier with my own work.&lt;/p&gt;

&lt;p&gt;That is the part I did not expect.&lt;/p&gt;

&lt;p&gt;I thought I was borrowing a farm.&lt;/p&gt;

&lt;p&gt;I was joining a barter economy where the same laptop that edits Miles during the day renders somebody else's sheep at night.&lt;/p&gt;

&lt;p&gt;Everyone is the customer and the supplier at the same time.&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%2F16sksp6hfjttlp4uibxf.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%2F16sksp6hfjttlp4uibxf.png" alt="Oprah meme: you lend a GPU, you get a frame, everybody gets a frame"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: strangers cannot run your scripts
&lt;/h2&gt;

&lt;p&gt;There is exactly one thing SheepIt asks of you, and it is the whole engineering problem.&lt;/p&gt;

&lt;p&gt;Your .blend has to run on a machine you have never seen, unattended, with none of your project around it.&lt;/p&gt;

&lt;p&gt;My scenes were not built for that.&lt;/p&gt;

&lt;p&gt;They are built by Python scripts that load a city, retarget animations, key thousands of values and bake the result into a &lt;code&gt;.blend&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On my laptop that file leans on a whole project folder:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Absolute paths&lt;/strong&gt; to textures, like &lt;code&gt;/home/lovestaco/pers/lr_demo/...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image sequences&lt;/strong&gt; for the animated screens in the city (the bugs board, the queue counters).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sound strips&lt;/strong&gt; for the voice-over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render settings&lt;/strong&gt; that my own scripts apply at render time.&lt;/p&gt;

&lt;p&gt;A stranger's machine has none of that.&lt;/p&gt;

&lt;p&gt;It cannot run my scripts, so settings I apply from code simply never happen.&lt;/p&gt;

&lt;p&gt;So I wrote a packer, &lt;code&gt;sheepit_pack.py&lt;/code&gt;, whose job is to turn a built scene into a hermetic one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pack every image into the .blend.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Textures travel inside the file now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy image sequences next to it with relative paths.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Blender cannot pack a sequence, so the folder rides along in the zip and the paths become &lt;code&gt;//assets/...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop the sound strips.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The voice-over and sound effects are mixed later with ffmpeg anyway, so the farm never needs them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bake the settings into the file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;1920x1080 at 100%, 64 samples, ray tracing at full resolution, motion blur on, PNG output.&lt;/p&gt;

&lt;p&gt;Then zip it with the &lt;code&gt;.blend&lt;/code&gt; at the root.&lt;/p&gt;

&lt;p&gt;My zips came out between 130 and 240 MB.&lt;/p&gt;

&lt;p&gt;SheepIt takes up to 2 GB, so that is a comfortable fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust nothing, verify everything
&lt;/h2&gt;

&lt;p&gt;Here is the thing about sending your scene to strangers.&lt;/p&gt;

&lt;p&gt;If it is broken, you find out hours later, when a stranger's machine has already burned your points on a hundred identical black frames.&lt;/p&gt;

&lt;p&gt;So every package goes through a gate before it leaves my laptop.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Built .blend] --&amp;gt; B[Pack&amp;lt;br/&amp;gt;images, relative paths, baked settings]
    B --&amp;gt; C[Zip&amp;lt;br/&amp;gt;blend at the root]
    C --&amp;gt; D[Extract into a clean folder]
    D --&amp;gt; E{Verify&amp;lt;br/&amp;gt;paths, settings, no sound}
    E --&amp;gt;|fail| X[Fix and repack]
    E --&amp;gt;|pass| F[Render 1 or 2 test frames&amp;lt;br/&amp;gt;from the extracted copy]
    F --&amp;gt; G[Upload the zip]
    G --&amp;gt; H[Community renders]
    H --&amp;gt; I[Download the frames zip]
    I --&amp;gt; J{Frame check&amp;lt;br/&amp;gt;count, gaps, size, black frames}
    J --&amp;gt;|fail| Y[Re-queue those frames]
    J --&amp;gt;|pass| K[Assemble with audio and subtitles]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef mine fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef farm fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef bad fill:#ff9a5c,stroke:#c4642a,color:#1a1a1a
    class E,J decision
    class A,B,C,D,F,K mine
    class G,H,I farm
    class X,Y bad&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The important part is the extraction.&lt;/p&gt;

&lt;p&gt;I do not test the original .blend.&lt;/p&gt;

&lt;p&gt;I unzip the package into a fresh temporary folder, the way the farm will see it, and run the checks there.&lt;/p&gt;

&lt;p&gt;Every external file must exist and be a relative path.&lt;/p&gt;

&lt;p&gt;No sound strips may be left.&lt;/p&gt;

&lt;p&gt;The resolution, samples, ray tracing, motion blur and PNG format must all match what I think I baked.&lt;/p&gt;

&lt;p&gt;Then I render one or two real frames from that extracted copy, with the baked settings.&lt;/p&gt;

&lt;p&gt;If those frames look right, the package is right.&lt;/p&gt;

&lt;p&gt;On my GTX 1650 those test frames take between 7 and 35 seconds at 1080p.&lt;/p&gt;

&lt;p&gt;Which is a quick way to tell the difference between "this will take a day" and "this will take a week" before anyone else spends a point on it.&lt;/p&gt;

&lt;p&gt;I run it as one command per part, and it stops with a loud error at the first problem.&lt;/p&gt;

&lt;p&gt;It also never deletes anything.&lt;/p&gt;

&lt;p&gt;An old package is moved aside, not overwritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug where the file was fine and the check was wrong
&lt;/h2&gt;

&lt;p&gt;The first time I ran the new packager for part 1, it died at the zip step with &lt;code&gt;ERROR: street_part_1_final.blend is not at the zip root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was at the zip root.&lt;/p&gt;

&lt;p&gt;I looked at the zip.&lt;/p&gt;

&lt;p&gt;The file was right there, first entry.&lt;/p&gt;

&lt;p&gt;The check looked like this, with the fix underneath:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# broken: dies on SIGPIPE even though the file is in the zip&lt;/span&gt;
unzip &lt;span class="nt"&gt;-Z1&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZIP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qx&lt;/span&gt; &lt;span class="s2"&gt;"street_part_1_final.blend"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; die &lt;span class="s2"&gt;"not at the zip root"&lt;/span&gt;

&lt;span class="c"&gt;# fixed: capture first, then grep&lt;/span&gt;
&lt;span class="nv"&gt;LIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;unzip &lt;span class="nt"&gt;-Z1&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZIP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qx&lt;/span&gt; &lt;span class="s2"&gt;"street_part_1_final.blend"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LIST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; die &lt;span class="s2"&gt;"not at the zip root"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;grep -q&lt;/code&gt; is polite about one thing: the moment it finds a match, it exits.&lt;/p&gt;

&lt;p&gt;That leaves &lt;code&gt;unzip&lt;/code&gt; writing the rest of the file list into a pipe nobody is reading, so it gets &lt;code&gt;SIGPIPE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;pipefail&lt;/code&gt; says that if any stage of the pipe fails, the whole pipe failed.&lt;/p&gt;

&lt;p&gt;So the file was found, and the check reported it missing.&lt;/p&gt;

&lt;p&gt;The fix is to stop piping.&lt;/p&gt;

&lt;p&gt;If you ever write &lt;code&gt;producer | grep -q&lt;/code&gt; under &lt;code&gt;set -o pipefail&lt;/code&gt;, remember this.&lt;/p&gt;

&lt;p&gt;It works until the producer has more output than the pipe buffer, and then it fails on the one day it matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  Render only what the film keeps
&lt;/h2&gt;

&lt;p&gt;This part is satisfying.&lt;/p&gt;

&lt;p&gt;The film is not one long take.&lt;/p&gt;

&lt;p&gt;Parts of the Blender scenes are cut from the final video (we removed a mural scene, the LED ticker and a subway lightbox), and the 2D product boards in part 3 are not rendered in Blender at all.&lt;/p&gt;

&lt;p&gt;If you upload a scene and ask for "frame 1 to the end", the community renders all of that for you, for free, and you throw half of it away.&lt;/p&gt;

&lt;p&gt;Rude to the sheep.&lt;/p&gt;

&lt;p&gt;So each package gets uploaded with exactly the frame ranges the film needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 0&lt;/strong&gt; is one range, 458 frames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; is three ranges, because three scenes are cut.&lt;/p&gt;

&lt;p&gt;That skips about 535 frames that never reach the final video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2&lt;/strong&gt; is one range, 2,441 frames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3&lt;/strong&gt; is the same zip uploaded twice, frames 1 to 664 and 2150 to 3661.&lt;/p&gt;

&lt;p&gt;The 2D boards sitting between them come from a Python script on my laptop in a couple of minutes.&lt;/p&gt;

&lt;p&gt;The whole film is &lt;strong&gt;8,229 frames&lt;/strong&gt;, or 4 minutes 34 seconds.&lt;/p&gt;

&lt;p&gt;Last post I said 7,300.&lt;/p&gt;

&lt;p&gt;The film grew.&lt;/p&gt;

&lt;p&gt;Films do that.&lt;/p&gt;
&lt;h2&gt;
  
  
  Assembling what comes back
&lt;/h2&gt;

&lt;p&gt;Each part comes home as a zip of PNG frames.&lt;/p&gt;

&lt;p&gt;A 1080p part is between 2 and 4 GB.&lt;/p&gt;

&lt;p&gt;I do not trust anything that arrives from a farm of strangers.&lt;/p&gt;

&lt;p&gt;So before a single frame goes into a video, a check script looks at the zip:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the zip intact?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the frame count exactly what the build says it should be, numbered from the first frame to the last, with no gaps and no extras?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does every PNG decode, at exactly the expected size?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is any frame entirely black?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An all-black frame is the farm's way of telling you that a worker had a bad day.&lt;/p&gt;

&lt;p&gt;If everything passes, the frames are hard-linked into a numbered folder (no copies, so a 4 GB set costs nothing extra), ffmpeg encodes the base video, and my assembly script mixes in the voice-over, sound effects, music, subtitles and the frame-number burn-in.&lt;/p&gt;

&lt;p&gt;That last one is for the team.&lt;/p&gt;

&lt;p&gt;When someone says "the thing at f 1201 is off", I can find it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Stale frames beat missing frames
&lt;/h2&gt;

&lt;p&gt;My render scripts skip frames that already exist, which is wonderful when a long render gets interrupted.&lt;/p&gt;

&lt;p&gt;It is terrible after you change the scene.&lt;/p&gt;

&lt;p&gt;Stale frames from the old version sit in the folder, the renderer says "already done", and your new film has the old shot spliced in.&lt;/p&gt;

&lt;p&gt;The rule I follow now: when the scene changes, the old frame folder gets moved aside (not deleted, never wiped), and the render starts into a clean one.&lt;/p&gt;
&lt;h2&gt;
  
  
  A draft at home, the real thing at the farm
&lt;/h2&gt;

&lt;p&gt;Here is the workflow that has been settled for the project, and it is the actual answer to the render bill from last post.&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%2Flyjj783bs0zbnb2oydw0.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%2Flyjj783bs0zbnb2oydw0.png" alt="Diagram of two loops: a cheap 360p review loop on my laptop that ends with a 10 MB copy for the team, and the 1080p SheepIt farm path of pack, verify, upload, community render, download and assemble"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drafts are local.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;360p, EEVEE, no ray tracing, no motion blur.&lt;/p&gt;

&lt;p&gt;About 0.7 seconds per frame, so part 2's 2,441 frames take roughly 30 minutes.&lt;/p&gt;

&lt;p&gt;The whole film is about two hours.&lt;/p&gt;

&lt;p&gt;Make chai, come back, watch Miles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finals are on the farm.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;1080p, 64 samples, ray traced reflections, motion blur.&lt;/p&gt;

&lt;p&gt;A day of laptop time, spread across a flock that does not need me.&lt;/p&gt;

&lt;p&gt;And between them sits the thing that makes drafts useful: a review copy the team can actually open.&lt;/p&gt;
&lt;h2&gt;
  
  
  Did Miles get his 1080p?
&lt;/h2&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;Two of the four parts have already come home at 1080p from the flock, a 2 GB zip of PNGs for the opening and 3.7 GB for part 2.&lt;/p&gt;

&lt;p&gt;Then the team's notes arrived and the film changed under them.&lt;/p&gt;

&lt;p&gt;So there is a fresh set of packages sitting verified and ready for the community, each one with a test frame already rendered from the extracted copy.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I would tell last week's me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not build a farm when a community already runs one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Four laptops taught me a lot about networking.&lt;/p&gt;

&lt;p&gt;A flock taught me that I had been solving the wrong problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make your scene hermetic before you make it fast.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the file only works on your machine, it works on one machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify the package, not the original.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Extract it into a clean folder and render a frame from that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a cheap draft loop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is the thing that lets a teammate say "this word is wrong" without it costing you a day.&lt;/p&gt;

&lt;p&gt;And a thank you to the strangers whose GPUs are going to render the final.&lt;/p&gt;

&lt;p&gt;You are not a sheep.&lt;/p&gt;

&lt;p&gt;You are the shepherd.&lt;/p&gt;

&lt;p&gt;I would love to hear from you in the comments.&lt;/p&gt;

&lt;p&gt;Have you ever lent your machine to a community farm, or used a community compute network, for a project?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow along, because next time I will show you the final cut.&lt;/strong&gt;&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Risk-Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is built for engineering teams that ship often, but also care about keeping production stable for customers. It approves some 40-50% of low-risk changes automatically, and it gives your team the tools to handle the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand risky changes faster.&lt;/li&gt;
&lt;li&gt;Fix problems in those changes, with human attention directed to the work that needs it.&lt;/li&gt;
&lt;li&gt;Enforce your engineering standards.&lt;/li&gt;
&lt;li&gt;Learn from production outcomes, such as incidents, rollbacks, and queue wait times.&lt;/li&gt;
&lt;li&gt;Improve the review process over time and make it more efficient.&lt;/li&gt;
&lt;li&gt;Connect to the major engineering tools that your team already uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Auto-Approve: Ship the Safe 50% Without a Human&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Auto-Approve Upto 50% of Your Pull Requests, Safely.&lt;/strong&gt; Risk scoring decides which MRs/PRs are safe enough to ship without a human, so your team only spends review time on risky and critical changes.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;demo.mp4&lt;/span&gt;
    
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview auto-approving a low-risk change while&lt;/i&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blender</category>
      <category>python</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>We Built a Blender Render Farm Out of Four Laptops</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:06:37 +0000</pubDate>
      <link>https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32</link>
      <guid>https://dev.to/lovestaco/we-built-a-blender-render-farm-out-of-four-laptops-1h32</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We built a render farm out of four laptops on a Saturday evening.&lt;/p&gt;

&lt;p&gt;It had a job queue, a web dashboard, a private network spanning three cities' worth of ISPs, and a shared drive that every machine could see.&lt;/p&gt;

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

&lt;p&gt;And then it lost a race against my own laptop, rendering alone.&lt;/p&gt;

&lt;p&gt;This is the story of that farm, the networking rabbit hole it dropped us into, and why a 130 MB file taking seven minutes to cross the internet ended the whole experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we left Miles
&lt;/h2&gt;

&lt;p&gt;If you have been following along, you know the setup.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7"&gt;part one&lt;/a&gt; I rigged a Spider-Man model in Blender without ever having opened Blender before.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" class="crayons-story__hidden-navigation-link"&gt;I Made Spider-Man Swing Without Animating a Single Frame&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/lovestaco" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" alt="lovestaco profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/lovestaco" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Athreya aka Maneshwar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Athreya aka Maneshwar
                
                
              
              &lt;div id="story-author-preview-content-4793045" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/lovestaco" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Athreya aka Maneshwar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" id="article-link-4793045"&gt;
          I Made Spider-Man Swing Without Animating a Single Frame
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/blender"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;blender&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;48&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              7&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            13 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;In &lt;a href="https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7"&gt;part two&lt;/a&gt; Miles moonwalked off a billboard and a Python test caught it.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7" class="crayons-story__hidden-navigation-link"&gt;Spider-Man Moonwalked Off a Billboard and Only My Test Suite Noticed&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/lovestaco" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" alt="lovestaco profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/lovestaco" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Athreya aka Maneshwar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Athreya aka Maneshwar
                
                
              
              &lt;div id="story-author-preview-content-4803195" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/lovestaco" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Athreya aka Maneshwar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 5&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7" id="article-link-4803195"&gt;
          Spider-Man Moonwalked Off a Billboard and Only My Test Suite Noticed
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/blender"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;blender&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/debugging"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;debugging&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;30&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            14 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Since then, the film has grown into a roughly four minute LiveReview explainer in three parts.&lt;/p&gt;

&lt;p&gt;That is about &lt;strong&gt;7,300 frames&lt;/strong&gt; at 30 fps.&lt;/p&gt;

&lt;p&gt;And every one of them has to be rendered by something.&lt;/p&gt;

&lt;h2&gt;
  
  
  360p is a lie we tell ourselves
&lt;/h2&gt;

&lt;p&gt;For review drafts, I render at 360p with EEVEE, Blender's real-time engine, at 8 samples with ray tracing and motion blur switched off.&lt;/p&gt;

&lt;p&gt;On my laptop's GTX 1650 that comes out to about one second per frame.&lt;/p&gt;

&lt;p&gt;A single part renders in 20 to 50 minutes.&lt;/p&gt;

&lt;p&gt;Go make chai, come back, watch Miles swing.&lt;/p&gt;

&lt;p&gt;Totally fine.&lt;/p&gt;

&lt;p&gt;Then you want the real thing.&lt;/p&gt;

&lt;p&gt;1080p, ray traced reflections on the wet street, proper sample counts.&lt;/p&gt;

&lt;p&gt;So I asked Claude to stop guessing and time actual frames from our scene.&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%2Fni80xsnuqvz05fkmatff.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%2Fni80xsnuqvz05fkmatff.png" alt="Bar chart of whole film render time at four quality levels, from about 2 hours at 360p to about 7 days with Cycles, plus a panel showing 2 workers are 1.3x faster and 3 workers are slower"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 1080p EEVEE versions we actually wanted sit between &lt;strong&gt;10 and 22 hours&lt;/strong&gt; for the whole film.&lt;/p&gt;

&lt;p&gt;Cycles, the path tracer, came in at 76 to 90 seconds per frame.&lt;/p&gt;

&lt;p&gt;That is a week.&lt;/p&gt;

&lt;p&gt;Of my laptop doing nothing else.&lt;/p&gt;

&lt;p&gt;With 6 GB of disk left.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Is it not even using the GPU?"
&lt;/h3&gt;

&lt;p&gt;That is the first question anyone asks, and it is a fair one.&lt;/p&gt;

&lt;p&gt;So we sampled &lt;code&gt;nvidia-smi&lt;/code&gt; during a render.&lt;/p&gt;

&lt;p&gt;Blender was on the card, using about 1.5 GB of the 4 GB VRAM, at roughly 44% utilization.&lt;/p&gt;

&lt;p&gt;At 360p the GPU finishes its part of the frame quickly and then sits there waiting for the CPU, which is busy posing the characters, evaluating the crowd and writing the JPEG.&lt;/p&gt;

&lt;p&gt;So the next obvious move was to run more Blender processes in parallel.&lt;/p&gt;

&lt;p&gt;Two workers were only 1.3x faster than one.&lt;/p&gt;

&lt;p&gt;Three workers were &lt;em&gt;slower than one&lt;/em&gt;, because three Blender processes fighting over eight cores and one GPU is not parallelism, it is a custody battle.&lt;/p&gt;

&lt;p&gt;The only free speed-up we found locally was rendering each frame range as a single animation call instead of calling &lt;code&gt;render()&lt;/code&gt; once per frame, which skips redoing scene setup every time.&lt;/p&gt;

&lt;p&gt;That bought about 22% on one worker.&lt;/p&gt;

&lt;p&gt;Nice, but nobody turns a 22 hour render into an overnight one with a 22% discount.&lt;/p&gt;

&lt;p&gt;The conclusion was pretty clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We did not need a faster script. We needed more GPUs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And there were three other laptops in the team, mostly idle on a Saturday evening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Flamenco
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://flamenco.blender.org/" rel="noopener noreferrer"&gt;Flamenco&lt;/a&gt; is Blender's own open source render farm software.&lt;/p&gt;

&lt;p&gt;It is a lot smaller than the phrase "render farm software" suggests.&lt;/p&gt;

&lt;p&gt;There are exactly two programs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flamenco Manager&lt;/strong&gt; is the brain.&lt;/p&gt;

&lt;p&gt;It holds the job queue, serves a web dashboard, and splits each job into tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flamenco Worker&lt;/strong&gt; is the muscle.&lt;/p&gt;

&lt;p&gt;It runs on each rendering machine, polls the manager for work, launches Blender in the background, and reports progress.&lt;/p&gt;

&lt;p&gt;Both are single Go binaries you download, extract and run.&lt;/p&gt;

&lt;p&gt;No database server, no Kubernetes, no YAML archaeology.&lt;/p&gt;

&lt;p&gt;When you submit a job, say "render frames 2370 to 2429 of this .blend", the manager cuts the range into &lt;strong&gt;chunks&lt;/strong&gt; of N frames.&lt;/p&gt;

&lt;p&gt;Each chunk becomes a task.&lt;/p&gt;

&lt;p&gt;Every idle worker grabs the next task, renders it, and asks for another, so a fast machine naturally ends up doing more of the work than a slow one.&lt;/p&gt;

&lt;p&gt;While a task runs, the worker keeps pinging the manager with a "may I keep running?" check, which is how cancelling a job actually reaches a laptop in another city.&lt;/p&gt;

&lt;p&gt;If a task fails, it gets retried on a different worker.&lt;/p&gt;

&lt;p&gt;After three failures on the same job, that worker gets blocklisted for the job so it stops eating chunks.&lt;/p&gt;

&lt;p&gt;Here is the farm we built:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    ME[My laptop&amp;lt;br/&amp;gt;Manager + Worker]
    SMB[(spidey share&amp;lt;br/&amp;gt;Samba)]
    TS{{Tailscale&amp;lt;br/&amp;gt;private network}}
    SH[Windows desktop&amp;lt;br/&amp;gt;later WSL]
    LI[pop-os laptop]
    RU[ASUS TUF laptop]

    ME --- SMB
    ME &amp;lt;--&amp;gt;|jobs + status| TS
    TS &amp;lt;--&amp;gt; SH
    TS &amp;lt;--&amp;gt; LI
    TS &amp;lt;--&amp;gt; RU
    SH -.-&amp;gt;|reads .blend, writes frames| SMB
    LI -.-&amp;gt;|reads .blend, writes frames| SMB
    RU -.-&amp;gt;|reads .blend, writes frames| SMB

    classDef host fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef net fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef store fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef peer fill:#ff9a5c,stroke:#c4642a,color:#1a1a1a
    class ME host
    class TS net
    class SMB store
    class SH,LI,RU peer&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Notice the dotted lines.&lt;/p&gt;

&lt;p&gt;That is the part that would end up mattering.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one assumption Flamenco makes
&lt;/h3&gt;

&lt;p&gt;Flamenco does not move your files around.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://flamenco.blender.org/usage/shared-storage/" rel="noopener noreferrer"&gt;shared storage docs&lt;/a&gt; are upfront about it: the manager and every worker must be able to see the same files, on a NAS or a file server or some other shared drive.&lt;/p&gt;

&lt;p&gt;The job just says "open this path", and it is your problem to make sure that path exists everywhere.&lt;/p&gt;

&lt;p&gt;On four laptops scattered across home connections, that sounds like a strange requirement.&lt;/p&gt;

&lt;p&gt;To be fair to Flamenco, a studio render farm lives on one fast LAN with one NAS, and this is a completely reasonable design there.&lt;/p&gt;

&lt;p&gt;Our "LAN" was four laptops on four home connections.&lt;/p&gt;

&lt;p&gt;Which brings us to the networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tailscale: a LAN made out of the internet
&lt;/h2&gt;

&lt;p&gt;My first worry was the obvious one: does this mean my machine has to push everything over my internet connection?&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; just creates a private network so all our laptops can talk to each other.&lt;/p&gt;

&lt;p&gt;Every machine runs the Tailscale client and logs into the same account (a "tailnet").&lt;/p&gt;

&lt;p&gt;Each machine gets a stable private address in the &lt;code&gt;100.x.y.z&lt;/code&gt; range, and every pair of machines talks over an encrypted &lt;a href="https://www.wireguard.com/" rel="noopener noreferrer"&gt;WireGuard&lt;/a&gt; tunnel.&lt;/p&gt;

&lt;p&gt;Tailscale's servers only do the coordination: who is in the network, what their public keys are, where they might be reachable.&lt;/p&gt;

&lt;p&gt;Your actual traffic is supposed to go machine to machine.&lt;/p&gt;

&lt;p&gt;"Supposed to" is doing a lot of work in that sentence, and we will get there.&lt;/p&gt;

&lt;p&gt;On my laptop, Claude set up a Samba share called &lt;code&gt;spidey&lt;/code&gt; and kept it bound to loopback only, so it was never exposed to my Wi-Fi or the internet.&lt;/p&gt;

&lt;p&gt;Then Tailscale itself forwarded the SMB port, so only machines on the tailnet could reach it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# on my laptop: Samba listens on 127.0.0.1 only,&lt;/span&gt;
&lt;span class="c"&gt;# Tailscale forwards tailnet traffic on 445 to it&lt;/span&gt;
tailscale serve &lt;span class="nt"&gt;--bg&lt;/span&gt; &lt;span class="nt"&gt;--tcp&lt;/span&gt; 445 tcp://127.0.0.1:445

&lt;span class="c"&gt;# on each Linux worker: mount it at the same path everywhere&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; cifs //100.76.212.8/spidey /mnt/spidey &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/root/.spidey_smb,uid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;,gid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;,vers&lt;span class="o"&gt;=&lt;/span&gt;3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first milestone was gloriously low-tech.&lt;/p&gt;

&lt;p&gt;Someone created a file from Windows.&lt;/p&gt;

&lt;p&gt;I created one from Linux.&lt;/p&gt;

&lt;p&gt;Both files showed up on both machines.&lt;/p&gt;

&lt;p&gt;We celebrated a text file like it was a product launch.&lt;/p&gt;
&lt;h2&gt;
  
  
  The path boss fight
&lt;/h2&gt;

&lt;p&gt;Having a shared folder is step one.&lt;/p&gt;

&lt;p&gt;Getting Blender on three operating systems to agree on what is &lt;em&gt;in&lt;/em&gt; it is the actual game.&lt;/p&gt;
&lt;h3&gt;
  
  
  Paths baked into the .blend
&lt;/h3&gt;

&lt;p&gt;Claude inspected &lt;code&gt;street_part2_b.blend&lt;/code&gt; and found 31 external asset references.&lt;/p&gt;

&lt;p&gt;25 of them were absolute paths starting with &lt;code&gt;/home/lovestaco/pers/lr_demo/...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Windows desktop has no &lt;code&gt;/home/lovestaco&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Neither does the pop-os laptop.&lt;/p&gt;

&lt;p&gt;The fix was to copy the project into the share and convert every reference to a Blender relative path (&lt;code&gt;//assets/...&lt;/code&gt;), resolved from wherever the .blend itself lives.&lt;/p&gt;

&lt;p&gt;After that, all three street files had zero absolute paths left.&lt;/p&gt;
&lt;h3&gt;
  
  
  Paths in the job itself
&lt;/h3&gt;

&lt;p&gt;The .blend location and output folder in a Flamenco job are a separate problem, and Flamenco does solve this one with &lt;strong&gt;two-way variables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The manager stores job paths with a placeholder, and each worker swaps in its own platform's value:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# flamenco-manager.yaml&lt;/span&gt;
&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;spidey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;is_twoway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/mnt/spidey&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;D:\spidey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two gotchas we learned the hard way.&lt;/p&gt;

&lt;p&gt;On Windows, Flamenco wants a &lt;strong&gt;drive letter&lt;/strong&gt;, not a &lt;code&gt;\\server\share&lt;/code&gt; UNC path.&lt;/p&gt;

&lt;p&gt;And on Linux, every machine has to use the &lt;em&gt;same&lt;/em&gt; mount point, because there is only one Linux value.&lt;/p&gt;

&lt;p&gt;That is why everyone mounted at &lt;code&gt;/mnt/spidey&lt;/code&gt;, and why the wizard rejected &lt;code&gt;~/spidey&lt;/code&gt; outright (it does not expand &lt;code&gt;~&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Creating &lt;code&gt;D:\spidey&lt;/code&gt; with &lt;code&gt;mklink&lt;/code&gt; also failed, which turned out to be because &lt;code&gt;mklink&lt;/code&gt; is a builtin of the old Command Prompt, not of PowerShell.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cmd /c mklink ...&lt;/code&gt; from an admin window fixed it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where is Blender?
&lt;/h3&gt;

&lt;p&gt;Then the Windows worker printed this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There was an error finding Blender on this system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Blender was on PATH.&lt;/p&gt;

&lt;p&gt;The shell had been restarted.&lt;/p&gt;

&lt;p&gt;It still could not find it.&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%2F0rmlt9px947pdav0b1pl.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%2F0rmlt9px947pdav0b1pl.png" alt="Monkey puppet side-eye meme about Flamenco on Windows looking for the program that opens .blend files instead of checking PATH"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://flamenco.blender.org/usage/variables/blender/" rel="noopener noreferrer"&gt;Blender variable docs&lt;/a&gt; explain why: on Windows the worker looks up which program is associated with &lt;code&gt;.blend&lt;/code&gt; files, rather than trusting PATH.&lt;/p&gt;

&lt;p&gt;The fix was gloriously petty.&lt;/p&gt;

&lt;p&gt;Create an empty junk .blend file, use it to set the file association, and the worker immediately reported &lt;code&gt;Blender 5.2.2 LTS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By 8:15 pm we had four workers awake on the dashboard:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;image pending:&lt;/strong&gt; Flamenco web dashboard showing four workers awake and the farm status idle&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My laptop, a Windows desktop, a pop-os laptop and an ASUS TUF gaming laptop.&lt;/p&gt;

&lt;p&gt;All on Blender 5.2.2.&lt;/p&gt;

&lt;p&gt;An actual render farm.&lt;/p&gt;

&lt;p&gt;On a Saturday.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first job
&lt;/h2&gt;

&lt;p&gt;We kept it small on purpose.&lt;/p&gt;

&lt;p&gt;60 frames of part 2b at 360p draft quality, split into three chunks of 20.&lt;/p&gt;

&lt;p&gt;Three workers picked up a chunk each within seconds.&lt;/p&gt;

&lt;p&gt;Frames started landing in the shared folder.&lt;/p&gt;

&lt;p&gt;And the farm genuinely worked.&lt;/p&gt;

&lt;p&gt;When the job finished, Claude compared frames rendered by three different machines and they were identical: same lighting, same colours, same Miles.&lt;/p&gt;

&lt;p&gt;That part is honestly very cool.&lt;/p&gt;

&lt;p&gt;Then we looked at &lt;em&gt;how long&lt;/em&gt; everything took.&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%2Fngjzysqonmmuy0glvru2.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%2Fngjzysqonmmuy0glvru2.png" alt="Bar chart of how long each worker spent reading the 115 MB .blend: about 3 seconds locally, over 3 minutes on the relayed laptops, 82 seconds then a failure on Windows, and about 6 minutes on WSL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My worker read the 115 MB .blend off its own disk in a few seconds and rendered at about 0.4 s per frame.&lt;/p&gt;

&lt;p&gt;The two relayed laptops spent over &lt;strong&gt;three minutes&lt;/strong&gt; just opening the file before rendering a single frame.&lt;/p&gt;

&lt;p&gt;A 20 frame chunk at 360p renders in seconds.&lt;/p&gt;

&lt;p&gt;So the farm was spending almost all its time downloading and almost none of it rendering.&lt;/p&gt;
&lt;h3&gt;
  
  
  "Missing DNA block"
&lt;/h3&gt;

&lt;p&gt;The Windows worker did something more dramatic.&lt;/p&gt;

&lt;p&gt;It spent 82 seconds reading the file and then Blender died with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ERROR Failed to read blend file 'D:\spidey...\street_part2_b_360p.blend': Missing DNA block&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This error is a great little window into how .blend files work.&lt;/p&gt;

&lt;p&gt;A .blend is basically a memory dump.&lt;/p&gt;

&lt;p&gt;It is a header followed by a long run of data blocks, each one a chunk of Blender's in-memory structs written straight to disk.&lt;/p&gt;

&lt;p&gt;To read those structs back, Blender needs to know their layout, which fields, which types, which offsets, and that description lives in a special block called &lt;strong&gt;DNA1&lt;/strong&gt; (the "SDNA").&lt;/p&gt;

&lt;p&gt;That is how a file saved years ago, by a Blender with differently shaped structs, can still be opened today.&lt;/p&gt;

&lt;p&gt;The catch is that DNA1 gets written near the &lt;strong&gt;end&lt;/strong&gt; of the file.&lt;/p&gt;

&lt;p&gt;So if a read dies partway through, Blender never reaches the block that explains everything before it, and you get "Missing DNA block".&lt;/p&gt;

&lt;p&gt;We checked the obvious thing first.&lt;/p&gt;

&lt;p&gt;The copy on Windows had the right size, 114,783,416 bytes, and once its SHA-256 finally finished (that alone took ages over the link), the hash matched mine exactly.&lt;/p&gt;

&lt;p&gt;The file was fine.&lt;/p&gt;

&lt;p&gt;The read was not.&lt;/p&gt;
&lt;h3&gt;
  
  
  The chunk size argument
&lt;/h3&gt;

&lt;p&gt;This sparked the best debate of the night.&lt;/p&gt;

&lt;p&gt;Claude suggested bigger chunks, so each worker would pay the slow load less often.&lt;/p&gt;

&lt;p&gt;The obvious pushback: wouldn't smaller chunks download faster, in more pieces, spread across more machines?&lt;/p&gt;

&lt;p&gt;The intuition makes sense if chunks map to pieces of the file, like a torrent.&lt;/p&gt;

&lt;p&gt;They do not.&lt;/p&gt;

&lt;p&gt;A chunk is a range of &lt;em&gt;frames&lt;/em&gt;, but every chunk opens the &lt;em&gt;entire&lt;/em&gt; scene.&lt;/p&gt;

&lt;p&gt;Smaller chunks mean more full reads, not smaller ones.&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%2Fu0k1nqkmpwgycq0m95aa.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%2Fu0k1nqkmpwgycq0m95aa.png" alt="Bernie Sanders meme: every 10 frame chunk, I am once again asking for the whole 115 MB .blend"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next question was a better one though: once the file has been read, isn't it done? Shouldn't only the first read be slow, since the OS caches the file?&lt;/p&gt;

&lt;p&gt;So we ran a second job with 10 frame chunks and watched the ASUS laptop take its second chunk.&lt;/p&gt;

&lt;p&gt;The first read took 3 min 02 s.&lt;/p&gt;

&lt;p&gt;The second took &lt;strong&gt;31 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Caching was real.&lt;/p&gt;

&lt;p&gt;Not instant, but real.&lt;/p&gt;

&lt;p&gt;On Windows, though, the cached retry failed with the same "Missing DNA block", so that SMB client and Blender were clearly not getting along.&lt;/p&gt;

&lt;p&gt;Moving that worker into WSL got past the error, and it promptly rendered at about 50 seconds per frame, which smells a lot like EEVEE running without access to the GPU.&lt;/p&gt;
&lt;h3&gt;
  
  
  A side quest: whose GPU is it anyway
&lt;/h3&gt;

&lt;p&gt;Even after loading, the remote workers were slow per frame.&lt;/p&gt;

&lt;p&gt;The pop-os laptop took about 2.3 s per frame against my 0.4 s, and its GPU sampling step alone was over 3x slower than mine.&lt;/p&gt;

&lt;p&gt;Its &lt;code&gt;lspci&lt;/code&gt; explained it: an NVIDIA GTX 1650 Mobile, &lt;em&gt;and&lt;/em&gt; an AMD Radeon Vega built into the Ryzen processor.&lt;/p&gt;

&lt;p&gt;Two graphics chips, like most gaming laptops.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;glxinfo&lt;/code&gt; said OpenGL was running on the AMD integrated chip by default, because that is what laptops do to save battery.&lt;/p&gt;

&lt;p&gt;EEVEE was happily rendering Miles on the weak chip while the GTX 1650 sat there.&lt;/p&gt;

&lt;p&gt;The fix is PRIME render offload, starting the worker with &lt;code&gt;__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia&lt;/code&gt; so Blender lands on the NVIDIA card.&lt;/p&gt;

&lt;p&gt;Worth knowing if you ever farm out renders to other people's laptops.&lt;/p&gt;

&lt;p&gt;Their GPU is not necessarily the GPU Blender gets.&lt;/p&gt;
&lt;h2&gt;
  
  
  Direct vs DERP (and what on earth is "blr")
&lt;/h2&gt;

&lt;p&gt;Back to the real problem, the reads.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tailscale status&lt;/code&gt; on my machine had one telling word at the end of each peer's line.&lt;/p&gt;

&lt;p&gt;The WSL worker was connected &lt;strong&gt;direct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The pop-os and ASUS laptops were going through &lt;strong&gt;relay "blr"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is what those two words actually mean.&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%2Frjaeajvpkn39j5t2ng03.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%2Frjaeajvpkn39j5t2ng03.png" alt="Diagram comparing a direct WireGuard UDP tunnel between the WSL worker and my laptop at about 97 ms ping with the pop-os laptop's traffic relayed through Tailscale's DERP server in Bengaluru at 58 to 415 ms, plus a note that it sits behind a hard NAT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Direct
&lt;/h3&gt;

&lt;p&gt;Tailscale's first choice is a direct WireGuard tunnel over UDP between the two machines.&lt;/p&gt;

&lt;p&gt;The hard part is that almost nobody's laptop has a public IP.&lt;/p&gt;

&lt;p&gt;You are behind your home router's NAT, and quite often behind your ISP's NAT too.&lt;/p&gt;

&lt;p&gt;So Tailscale does &lt;strong&gt;NAT traversal&lt;/strong&gt;, also known as hole punching.&lt;/p&gt;

&lt;p&gt;Each machine discovers what its public IP and port look like from the outside, the coordination server shares those with the peer, and both sides fire UDP packets at each other at the same time.&lt;/p&gt;

&lt;p&gt;Each router sees an outgoing packet, creates a mapping for the reply, and if the timing and ports line up, the two outgoing "holes" meet in the middle.&lt;/p&gt;

&lt;p&gt;Tailscale's &lt;a href="https://tailscale.com/blog/how-nat-traversal-works" rel="noopener noreferrer"&gt;How NAT traversal works&lt;/a&gt; is one of the best networking posts on the internet if you want the full version.&lt;/p&gt;
&lt;h3&gt;
  
  
  DERP
&lt;/h3&gt;

&lt;p&gt;When hole punching fails, Tailscale falls back to &lt;a href="https://tailscale.com/kb/1232/derp-servers" rel="noopener noreferrer"&gt;DERP&lt;/a&gt;, short for &lt;strong&gt;Designated Encrypted Relay for Packets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;DERP servers are relays that Tailscale runs around the world.&lt;/p&gt;

&lt;p&gt;Your packets go to the relay over HTTPS and it forwards them to the other machine.&lt;/p&gt;

&lt;p&gt;They stay WireGuard-encrypted end to end, so the relay cannot read them, it just shuffles opaque bytes.&lt;/p&gt;

&lt;p&gt;DERP regions are named after airport-style city codes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"blr" is Bengaluru.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So "relay blr" just means "that laptop's packets to me were taking a detour through Tailscale's Bengaluru relay".&lt;/p&gt;

&lt;p&gt;Direct is faster for the reasons you would guess.&lt;/p&gt;

&lt;p&gt;A relay adds an extra hop, it is a shared server rather than your own path, and it runs over TCP, so a congested relay hurts you in ways a direct UDP path does not.&lt;/p&gt;

&lt;p&gt;Pings from the pop-os laptop through blr ranged from &lt;strong&gt;58 ms to 415 ms&lt;/strong&gt;, and one test run stalled after receiving about 159 MB, with even a plain &lt;code&gt;ls&lt;/code&gt; on the share hanging for two minutes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why it could not go direct
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tailscale netcheck&lt;/code&gt; on the pop-os laptop said:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* UDP: true
* IPv6: no, but OS has support
* MappingVariesByDestIP: true
* PortMapping: UPnP
* Nearest DERP: Bengaluru (blr: 48.6ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important line is &lt;code&gt;MappingVariesByDestIP: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a "hard" NAT.&lt;/p&gt;

&lt;p&gt;Its router assigns a &lt;em&gt;different&lt;/em&gt; public port for every destination, so the port my laptop learns about is not the port that router will actually accept traffic on from me.&lt;/p&gt;

&lt;p&gt;Hole punching needs at least one side to be predictable.&lt;/p&gt;

&lt;p&gt;Mine was the easy kind, but my router offered no UPnP or NAT-PMP to open a port automatically, and that side had no IPv6 to sidestep the whole mess.&lt;/p&gt;

&lt;p&gt;Here is roughly how Tailscale decides:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Two peers want to talk] --&amp;gt; B{Either side has&amp;lt;br/&amp;gt;a public, open port?}
    B --&amp;gt;|yes| D[Direct WireGuard over UDP]
    B --&amp;gt;|no| C{Both NATs map ports&amp;lt;br/&amp;gt;predictably?}
    C --&amp;gt;|yes| E{Hole punch succeeds?}
    C --&amp;gt;|no, hard NAT| F{IPv6 on both sides?}
    E --&amp;gt;|yes| D
    E --&amp;gt;|no| G[Relay through nearest DERP]
    F --&amp;gt;|yes| D
    F --&amp;gt;|no| G

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef good fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef bad fill:#ff9a5c,stroke:#c4642a,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    class B,C,E,F decision
    class D good
    class G bad
    class A start&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The suggested fix was forwarding UDP 41641 on my router so the hard NAT side would have an open door to knock on.&lt;/p&gt;
&lt;h3&gt;
  
  
  The twist: direct did not save us either
&lt;/h3&gt;

&lt;p&gt;Then we noticed something awkward.&lt;/p&gt;

&lt;p&gt;The WSL worker &lt;em&gt;was&lt;/em&gt; on a direct connection.&lt;/p&gt;

&lt;p&gt;And it still took about &lt;strong&gt;six minutes&lt;/strong&gt; to load the file.&lt;/p&gt;

&lt;p&gt;So we measured my connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0.65 MB/s upload.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;About 5 Mbit/s.&lt;/p&gt;

&lt;p&gt;Every worker in this farm was reading the .blend from my laptop.&lt;/p&gt;

&lt;p&gt;Which means every byte, direct or relayed, had to squeeze out through my upload.&lt;/p&gt;

&lt;p&gt;At 0.65 MB/s, one 115 MB read takes about three minutes.&lt;/p&gt;

&lt;p&gt;Two workers reading at once, about six.&lt;/p&gt;

&lt;p&gt;That worker took in 136 MB in roughly six minutes, about 0.38 MB/s, which is exactly what half of my upload looks like.&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%2Fsw5jzr3skue20nzqng19.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%2Fsw5jzr3skue20nzqng19.png" alt="Waiting skeleton meme: remote worker loading the 115 MB .blend over my 5 Mbit/s upload"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fixing the hard NAT would have shortened the detour.&lt;/p&gt;

&lt;p&gt;It would not have widened the pipe.&lt;/p&gt;

&lt;p&gt;I switched to a faster Wi-Fi network for one more run, which bumped my upload to about 2.15 MB/s, and the remote first loads dropped to about a minute and a half.&lt;/p&gt;

&lt;p&gt;Better.&lt;/p&gt;

&lt;p&gt;But in that same run my laptop alone finished four of the six chunks while the remote workers were still loading.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why we pulled the plug
&lt;/h2&gt;

&lt;p&gt;For a remote worker to help, two things have to be true.&lt;/p&gt;

&lt;p&gt;It has to get the scene fast, and it has to render fast once it has it.&lt;/p&gt;

&lt;p&gt;Ours had neither.&lt;/p&gt;

&lt;p&gt;The scene came through my home upload, partly via a relay, and the laptops that did load it were either on the wrong GPU, in WSL with no GPU, or tripping over SMB on Windows.&lt;/p&gt;

&lt;p&gt;Every fix we found was real, and every one of them was its own project.&lt;/p&gt;

&lt;p&gt;Copy the project to every laptop ahead of time and lose the "one shared folder" simplicity that made the setup appealing.&lt;/p&gt;

&lt;p&gt;Fight every home router.&lt;/p&gt;

&lt;p&gt;Fix every laptop's GPU setup.&lt;/p&gt;

&lt;p&gt;Make sure every laptop stays awake, plugged in and on Wi-Fi for ten hours.&lt;/p&gt;

&lt;p&gt;So at 9:52 pm we shut it all down.&lt;/p&gt;

&lt;p&gt;Flamenco manager and worker stopped, Samba stopped and disabled, the Tailscale forward turned off, Tailscale itself stopped.&lt;/p&gt;

&lt;p&gt;Nothing deleted, just switched off.&lt;/p&gt;

&lt;p&gt;A few things I would tell past me before this Saturday:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A render farm is a file distribution problem wearing a GPU costume.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The render was the easy part.&lt;/p&gt;

&lt;p&gt;Getting the same 130 MB to every machine, reliably, was the whole job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure your upload before you build anything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Five minutes with a speed test would have predicted most of this evening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Direct" is about the route, not the speed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A perfect peer-to-peer tunnel is still capped by the slowest pipe on either end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check which GPU Blender is actually using.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dual-GPU laptops will quietly hand you the integrated chip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flamenco is fine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It assumes a studio LAN with a NAS, and we gave it four home connections and a laptop pretending to be a NAS.&lt;/p&gt;
&lt;h2&gt;
  
  
  So did Miles ever get his 1080p?
&lt;/h2&gt;

&lt;p&gt;We really wanted this one to work.&lt;/p&gt;

&lt;p&gt;Four laptops, one Saturday, our own little render farm.&lt;/p&gt;

&lt;p&gt;It is a great story, it just is not a fast one.&lt;/p&gt;

&lt;p&gt;But the film still needs rendering, and we did eventually crack it, by spreading the work across multiple providers instead of multiple laptops.&lt;/p&gt;

&lt;p&gt;That is the next post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow along so you do not miss how we finally got Miles rendered.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And I would love to hear from you in the comments.&lt;/p&gt;

&lt;p&gt;Have you ever tried to farm out a render, or any big compute job, across machines you did not own?&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Risk-Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is built for engineering teams that ship often, but also care about keeping production stable for customers. It approves some 40-50% of low-risk changes automatically, and it gives your team the tools to handle the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand risky changes faster.&lt;/li&gt;
&lt;li&gt;Fix problems in those changes, with human attention directed to the work that needs it.&lt;/li&gt;
&lt;li&gt;Enforce your engineering standards.&lt;/li&gt;
&lt;li&gt;Learn from production outcomes, such as incidents, rollbacks, and queue wait times.&lt;/li&gt;
&lt;li&gt;Improve the review process over time and make it more efficient.&lt;/li&gt;
&lt;li&gt;Connect to the major engineering tools that your team already uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Auto-Approve: Ship the Safe 50% Without a Human&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Auto-Approve Upto 50% of Your Pull Requests, Safely.&lt;/strong&gt; Risk scoring decides which MRs/PRs are safe enough to ship without a human, so your team only spends review time on risky and critical changes.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;demo.mp4&lt;/span&gt;
    
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview auto-approving a low-risk change while&lt;/i&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fmatye5a3kc7c9w0ckfv8.png" alt="LiveReview Banner"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blender</category>
      <category>networking</category>
      <category>tailscale</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Stack I'd Need for Claude to Direct a Whole YouTube Video in Blender</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Fri, 09 Oct 2026 18:10:15 +0000</pubDate>
      <link>https://dev.to/lovestaco/the-stack-id-need-for-claude-to-direct-a-whole-youtube-video-in-blender-2ekd</link>
      <guid>https://dev.to/lovestaco/the-stack-id-need-for-claude-to-direct-a-whole-youtube-video-in-blender-2ekd</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two posts ago, I had never opened Blender.&lt;/p&gt;

&lt;p&gt;Since then, Miles Morales has swung on a web, yanked a DEV board into frame, and &lt;a href="https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7"&gt;moonwalked 6.5 metres off the edge of a billboard&lt;/a&gt; without anyone noticing.&lt;/p&gt;

&lt;p&gt;Every one of those shots was me and Claude Code, pair programming a 3D scene one prompt at a time.&lt;/p&gt;

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

&lt;p&gt;It is also slow, because I am still the one holding the whole movie in my head.&lt;/p&gt;

&lt;p&gt;So this post is about the next question, the one I keep circling back to at 1 a.m.:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What would it actually take for me to type one paragraph and get a finished YouTube video out the other end?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "Claude helps me in Blender".&lt;/p&gt;

&lt;p&gt;Claude &lt;em&gt;directs&lt;/em&gt; the thing.&lt;/p&gt;

&lt;p&gt;I went digging through every MCP server, Claude Code skill, mocap source and audio model I could find, checked the GitHub stars so we know what is battle tested and what is a weekend project, and wrote down the stack I would build.&lt;/p&gt;

&lt;p&gt;Spoiler: Blender is maybe a third of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job description: director, not animator
&lt;/h2&gt;

&lt;p&gt;The single most useful thing I learned in &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7"&gt;part one&lt;/a&gt; was not about rigging.&lt;/p&gt;

&lt;p&gt;It was about where the LLM is good and where it is hopeless.&lt;/p&gt;

&lt;p&gt;Claude was great at writing &lt;code&gt;bpy&lt;/code&gt; code, relinking broken textures, sequencing clips on a timeline, and stitching root motion between them.&lt;/p&gt;

&lt;p&gt;Claude was never once asked to invent how a human body lands from a jump.&lt;/p&gt;

&lt;p&gt;That came from Mixamo, which came from a real performer in a mocap suit.&lt;/p&gt;

&lt;p&gt;And that split is the whole architecture.&lt;/p&gt;

&lt;p&gt;An LLM is a brilliant writer of structure and code, a decent critic of a still image, and a terrible source of biomechanics.&lt;/p&gt;

&lt;p&gt;So every part of a video should go to whoever is actually good at it:&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%2Fwaaa81su3k8vh6sk5v97.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%2Fwaaa81su3k8vh6sk5v97.png" alt="Three columns: Claude writes the shot list, scripts, camera moves and the edit; Claude fetches body motion, sets, the character and mouth shapes; Claude orders voice, SFX, music and background props from specialist models" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it moves like a human, fetch it.&lt;/p&gt;

&lt;p&gt;If it is code, write it.&lt;/p&gt;

&lt;p&gt;If it is a sound, order it from a model that does nothing but sound.&lt;/p&gt;

&lt;p&gt;Here is the whole stack with that rule applied:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    ME[One paragraph from me] --&amp;gt; DIR[Claude Code&amp;lt;br/&amp;gt;director skill]
    DIR --&amp;gt; SL[Shot list&amp;lt;br/&amp;gt;YAML]
    SL --&amp;gt; BR[Blender bridge&amp;lt;br/&amp;gt;one MCP server]
    SL --&amp;gt; MO[Motion library&amp;lt;br/&amp;gt;Mixamo, CMU, Rokoko]
    SL --&amp;gt; AS[Assets&amp;lt;br/&amp;gt;Poly Haven, Sketchfab, image to 3D]
    SL --&amp;gt; AU[Audio&amp;lt;br/&amp;gt;TTS, SFX, music]
    AU --&amp;gt; LS[Rhubarb&amp;lt;br/&amp;gt;mouth shapes]
    MO --&amp;gt; BR
    AS --&amp;gt; BR
    LS --&amp;gt; BR
    BR --&amp;gt; BL[Blender, headless&amp;lt;br/&amp;gt;EEVEE drafts, frame sequences]
    BL --&amp;gt; QA{Checks pass?&amp;lt;br/&amp;gt;eyes + asserts}
    QA --&amp;gt;|no| DIR
    QA --&amp;gt;|yes| FF[FFmpeg&amp;lt;br/&amp;gt;frames + stems]
    AU --&amp;gt; FF
    FF --&amp;gt; YT[YouTube-ready MP4]

    classDef human fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef brain fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef fetch fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef order fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a
    classDef engine fill:#6ea8ff,stroke:#2f62c4,color:#1a1a1a
    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a

    class ME,YT human
    class DIR,SL brain
    class MO,AS,LS fetch
    class AU order
    class BR,BL,FF engine
    class QA decision&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Let's walk it bottom up, starting with the piece everyone argues about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: the Blender bridge (pick exactly one)
&lt;/h2&gt;

&lt;p&gt;This is the MCP server that lets Claude reach into a running Blender and press buttons by writing Python.&lt;/p&gt;

&lt;p&gt;There are now a lot of them.&lt;/p&gt;

&lt;p&gt;Here are the four I looked at seriously, with stars as of this week:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bridge&lt;/th&gt;
&lt;th&gt;What it is good at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/ahujasid/mcp-for-blender" rel="noopener noreferrer"&gt;mcp-for-blender&lt;/a&gt; ⭐ (~30.2k) (formerly &lt;code&gt;blender-mcp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The one I used in parts 1 and 2. Code execution, viewport screenshots, plus Poly Haven, Sketchfab, Poly Pizza, Hyper3D Rodin, Hunyuan3D and Tripo built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://www.blender.org/lab/mcp-server/" rel="noopener noreferrer"&gt;Official Blender MCP&lt;/a&gt; ⭐ by Blender Lab&lt;/td&gt;
&lt;td&gt;Understanding a scene: explaining node setups, finding what uses a material, cleanup. Blender's page lists 5.1 or newer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/carlosh7/blender-mcp" rel="noopener noreferrer"&gt;carlosh7/blender-mcp&lt;/a&gt; ⭐ (8 stars)&lt;/td&gt;
&lt;td&gt;248 tools across 14 categories, 371 commits, 479 passing tests, MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/minihellboy/claude-blender" rel="noopener noreferrer"&gt;claude-blender&lt;/a&gt; ⭐ (5 stars)&lt;/td&gt;
&lt;td&gt;Undo checkpoints, camera auto-framing, local TripoSR and Shap-E generation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official one is official in the sense that Blender developers built it, and &lt;a href="https://academy.claude.com/tutorials/using-the-blender-connector-in-claude" rel="noopener noreferrer"&gt;Anthropic announced it&lt;/a&gt; as a Claude connector this year.&lt;/p&gt;

&lt;p&gt;Read its own page though, and it is pitched as a way to &lt;em&gt;understand&lt;/em&gt; complex setups, not to churn out a film.&lt;/p&gt;

&lt;p&gt;It also carries the most honest warning I have seen on any MCP page: the server "will execute LLM generated code in Blender without any guards in place", and Blender suggests a VM or a machine without sensitive data on it.&lt;/p&gt;

&lt;p&gt;That applies to every server in the table, by the way.&lt;/p&gt;

&lt;p&gt;They all ship some flavour of "run this Python", and that is the tool doing the real work.&lt;/p&gt;

&lt;p&gt;Which brings me to the 248 tool one.&lt;/p&gt;

&lt;p&gt;It is clearly a lot of careful work, with real tests and CI.&lt;/p&gt;

&lt;p&gt;But it made me realise that &lt;strong&gt;tool count is not a feature&lt;/strong&gt;.&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%2Fp35q0tnnsofch3nw1f86.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%2Fp35q0tnnsofch3nw1f86.png" alt="American Chopper argument meme about 248 MCP tools versus one tool that runs Python" width="360" height="1012"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every tool an MCP server exposes comes with a schema: a name, a description, a JSON shape for its arguments.&lt;/p&gt;

&lt;p&gt;Many clients put all of those in the context window on every single turn, whether you use them or not.&lt;/p&gt;

&lt;p&gt;Some, Claude Code included, can defer them and search on demand, which fixes the space problem.&lt;/p&gt;

&lt;p&gt;It does not fix the &lt;em&gt;picking&lt;/em&gt; problem.&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%2F7dfn3megrcnxxmb6j9jq.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%2F7dfn3megrcnxxmb6j9jq.png" alt="A context window mostly filled by tool schemas versus one mostly free with skill descriptions, plus five near-identical tool names for moving a cube" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If there are five slightly different ways to move a cube, the model has to choose one, and every choice is a chance to choose wrong.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;execute_blender_code&lt;/code&gt; and a model that writes good &lt;code&gt;bpy&lt;/code&gt; already covers all 248, because every button in Blender is a thin skin over &lt;code&gt;bpy&lt;/code&gt; anyway.&lt;/p&gt;

&lt;p&gt;So my pick is boring: &lt;strong&gt;mcp-for-blender as the one bridge&lt;/strong&gt;, installed with &lt;code&gt;claude mcp add blender uvx mcp-for-blender&lt;/code&gt;, and the official one kept around for "why is this scene slow" archaeology.&lt;/p&gt;

&lt;p&gt;And I am stealing the best idea from the 5 star repo without installing it.&lt;/p&gt;

&lt;p&gt;Checkpoints before risky edits are a great idea.&lt;/p&gt;

&lt;p&gt;In part two the &lt;code&gt;.blend&lt;/code&gt; file became a build artifact generated by scripts, so my checkpoints are just &lt;code&gt;git commit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: skills, not more tools
&lt;/h2&gt;

&lt;p&gt;If tools are expensive, how do you teach Claude cinematography?&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;Claude Code skills&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A skill is a folder in &lt;code&gt;.claude/skills/&lt;/code&gt; with a &lt;code&gt;SKILL.md&lt;/code&gt; inside: a one line description that is always visible, and a body of instructions and scripts that only loads when the task needs it.&lt;/p&gt;

&lt;p&gt;It is the lazy import of prompting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kevinbadi/blender-skills" rel="noopener noreferrer"&gt;blender-skills&lt;/a&gt; (~143 stars) is a good example of the idea.&lt;/p&gt;

&lt;p&gt;It has camera moves as skills, &lt;code&gt;turntable&lt;/code&gt;, &lt;code&gt;slow-zoom&lt;/code&gt;, &lt;code&gt;dolly-rotate&lt;/code&gt;, &lt;code&gt;crane-shot&lt;/code&gt;, &lt;code&gt;perfect-loop&lt;/code&gt;, plus image to 3D through Meshy and a toolkit that does Mixamo retargeting.&lt;/p&gt;

&lt;p&gt;It is aimed at product shots, so I would not install it wholesale for a superhero short.&lt;/p&gt;

&lt;p&gt;I would copy its shape.&lt;/p&gt;

&lt;p&gt;The thing I actually want is a small library of &lt;strong&gt;film vocabulary&lt;/strong&gt;, each word backed by tested code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;establishing_wide&lt;/code&gt;, &lt;code&gt;push_in&lt;/code&gt;, &lt;code&gt;pull_out&lt;/code&gt;, &lt;code&gt;orbit&lt;/code&gt;, &lt;code&gt;crane_up&lt;/code&gt;, &lt;code&gt;tracking&lt;/code&gt;, &lt;code&gt;over_the_shoulder&lt;/code&gt;, &lt;code&gt;handheld_shake&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rainy_night&lt;/code&gt;, &lt;code&gt;golden_hour&lt;/code&gt;, &lt;code&gt;villain_red&lt;/code&gt;, &lt;code&gt;neon_street&lt;/code&gt;, &lt;code&gt;soft_closeup_key&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then "start wide, track him to the ledge, push in slowly as he says the line" stops being a creative writing exercise for the model.&lt;/p&gt;

&lt;p&gt;It becomes three skill calls with numbers.&lt;/p&gt;

&lt;p&gt;Same idea applies to lighting.&lt;/p&gt;

&lt;p&gt;A preset that is right once is right forever, and nobody has to rediscover three point lighting at 2 a.m. because Miles's suit rendered as a black blob again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: motion, fetched not generated
&lt;/h2&gt;

&lt;p&gt;This is where most "AI made my animation" demos fall apart, and it is the layer I am most opinionated about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not let the LLM keyframe a human.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It will produce something that technically moves and spiritually does not.&lt;/p&gt;

&lt;p&gt;Real motion has weight shifts, overshoot, a little settle at the end of a landing.&lt;/p&gt;

&lt;p&gt;Mocap has all of that for free, because a person did it.&lt;/p&gt;

&lt;p&gt;The sources I would wire in, in order of effort:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.mixamo.com" rel="noopener noreferrer"&gt;Mixamo&lt;/a&gt;.&lt;/strong&gt; Free, auto-rigs a T-posed character from five markers, thousands of clips. I downloaded 78 of them one click at a time in part one, and I would do it again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="http://mocap.cs.cmu.edu/" rel="noopener noreferrer"&gt;CMU Motion Capture Database&lt;/a&gt;.&lt;/strong&gt; About 2,500 free clips, with a &lt;a href="https://github.com/una-dinosauria/cmu-mocap" rel="noopener noreferrer"&gt;BVH mirror on GitHub&lt;/a&gt; (~203 stars). Needs a retargeter, which part one built and part two debugged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.rokoko.com/integrations/blender" rel="noopener noreferrer"&gt;Rokoko&lt;/a&gt;.&lt;/strong&gt; Their Blender add-on is free and does retargeting. Their free plan includes Rokoko Vision, single camera AI mocap. That last one is the exciting bit: the motion Mixamo does not have, I can act out in my living room with a webcam.&lt;/p&gt;

&lt;p&gt;And then the controls.&lt;/p&gt;

&lt;p&gt;In part one I dragged Miles's hips down and his whole body sank through the floor like a ghost, because the rig had bones but no IK.&lt;/p&gt;

&lt;p&gt;Two fixes, depending on budget:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://extensions.blender.org/add-ons/mixamo-rig/" rel="noopener noreferrer"&gt;Mixamo Rig&lt;/a&gt;&lt;/strong&gt; is a free, GPL extension that builds a proper IK control rig on top of a Mixamo skeleton and bakes animation in and out of it. Check the version range on the listing before you build on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://superhivemarket.com/products/auto-rig-pro" rel="noopener noreferrer"&gt;Auto-Rig Pro&lt;/a&gt;&lt;/strong&gt; by Artell is the paid, serious option, about $50, used in shipped games like Manor Lords, with rigging, retargeting between skeletons and facial rigs in one place.&lt;/p&gt;

&lt;p&gt;If I spend money anywhere in this stack, it is here.&lt;/p&gt;

&lt;p&gt;Rigging is the one job where a mistake quietly poisons every shot that comes after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: the face, or the close-up problem
&lt;/h2&gt;

&lt;p&gt;Here is a thing I did not appreciate until I tried to frame a close-up.&lt;/p&gt;

&lt;p&gt;In a wide shot, nobody looks at the face.&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%2F0jqdpc3z5eqc0yvrn41m.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%2F0jqdpc3z5eqc0yvrn41m.png" alt="See? Nobody Cares meme about a broken face rig in a wide shot" width="360" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the camera pushes in for the line, and the face is the &lt;em&gt;only&lt;/em&gt; thing on screen.&lt;/p&gt;

&lt;p&gt;A mouth flapping open and closed on a timer reads instantly as fake.&lt;/p&gt;

&lt;p&gt;So does a character who never blinks, or whose eyes are locked dead ahead like a mannequin's.&lt;/p&gt;

&lt;p&gt;The good news is that most of facial performance can be fetched too, and the most important piece is wonderfully small.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DanielSWolf/rhubarb-lip-sync" rel="noopener noreferrer"&gt;Rhubarb Lip Sync&lt;/a&gt; (~2.6k stars) is a command line tool that listens to a voice recording and writes out a timeline of mouth shapes.&lt;/p&gt;

&lt;p&gt;It uses nine shapes, A to H plus X for rest, the same small set classic cartoon studios used, and you can hand it the script with &lt;code&gt;--dialogFile&lt;/code&gt; so it knows the words instead of guessing.&lt;/p&gt;

&lt;p&gt;It says it is built for 2D, but the output is just JSON with timestamps, and a 3D face can use it just as well.&lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://github.com/scaredyfish/blender-rhubarb-lipsync" rel="noopener noreferrer"&gt;Blender add-on&lt;/a&gt; for it (~228 stars), but it has not been touched since 2023.&lt;/p&gt;

&lt;p&gt;The glue is short enough that I would rather have Claude own it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rhubarb -f json -d line.txt -o line.json line.wav
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bpy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;cues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mouthCues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Miles_Head&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape_keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_blocks&lt;/span&gt;
&lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpy&lt;/span&gt;&lt;span class="p"&gt;.&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;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;   &lt;span class="c1"&gt;# line begins on frame 120
&lt;/span&gt;
&lt;span class="c1"&gt;# Rhubarb's letters -&amp;gt; this model's shape key names
&lt;/span&gt;&lt;span class="n"&gt;VISEME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MBP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;G&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;H&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cue&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;VISEME&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;VISEME&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;keyframe_insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is the whole trick: audio in, a keyframe per mouth change out.&lt;/p&gt;

&lt;p&gt;Blender's default Bezier interpolation eases between the keys, so the mouth glides instead of snapping.&lt;/p&gt;

&lt;p&gt;If your model drives its mouth with face bones instead of shape keys, like the 143 bone face rig on my Miles, the same loop poses bones instead.&lt;/p&gt;

&lt;p&gt;Blinks and eye darts are even cheaper.&lt;/p&gt;

&lt;p&gt;A blink every two to six seconds, randomised, and a tiny eye flick toward whatever the character is reacting to, is a dozen lines of procedural keyframing, and it is the difference between a person and a wax figure.&lt;/p&gt;

&lt;p&gt;Notice the dependency hiding here, too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lip sync needs the audio to exist first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which means the voice is not post production.&lt;/p&gt;

&lt;p&gt;It is pre production.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 5: assets, mostly fetched
&lt;/h2&gt;

&lt;p&gt;Same rule again: generate only what nobody has made yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://polyhaven.com" rel="noopener noreferrer"&gt;Poly Haven&lt;/a&gt;.&lt;/strong&gt; Thousands of CC0 HDRIs, textures and models. CC0 means no attribution and no strings. mcp-for-blender searches and imports from it directly, and an HDRI alone fixes half of "this scene looks fake".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://sketchfab.com" rel="noopener noreferrer"&gt;Sketchfab&lt;/a&gt;.&lt;/strong&gt; Huge, and also where my Miles came from. Read every license, because they vary model to model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image or text to 3D&lt;/strong&gt;, for the props nobody has made:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Tencent-Hunyuan/Hunyuan3D-2" rel="noopener noreferrer"&gt;Hunyuan3D-2&lt;/a&gt; (~15k stars) and &lt;a href="https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1" rel="noopener noreferrer"&gt;Hunyuan3D-2.1&lt;/a&gt; (~4.1k stars, adds PBR materials), from Tencent, runnable locally if your GPU is braver than my 4 GB GTX 1650&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/VAST-AI-Research/TripoSR" rel="noopener noreferrer"&gt;TripoSR&lt;/a&gt; (~7k stars), fast single image reconstruction&lt;/li&gt;
&lt;li&gt;Hosted options like Hyper3D Rodin and Tripo, already wired into mcp-for-blender&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest caveat from someone who just learned what skin weights are.&lt;/p&gt;

&lt;p&gt;Generated meshes are usually a dense soup of triangles.&lt;/p&gt;

&lt;p&gt;That is fine for a mailbox in the background.&lt;/p&gt;

&lt;p&gt;It is not fine for anything that has to &lt;em&gt;bend&lt;/em&gt;, because rigging wants clean edge loops around elbows and knees, and a triangle soup deforms like wet cardboard.&lt;/p&gt;

&lt;p&gt;So: generated props, sourced or hand built heroes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 6: audio is half the movie
&lt;/h2&gt;

&lt;p&gt;Every engineer I know who tries video makes the same mistake I did.&lt;/p&gt;

&lt;p&gt;They spend three days on render settings and zero minutes on sound.&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%2F6eqrn93pxbztu777n1gu.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%2F6eqrn93pxbztu777n1gu.png" alt="This Is Where I'd Put My Trophy meme: this is where I'd put my sound design, if I had any" width="360" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Put a whoosh on a jump, a &lt;em&gt;thwip&lt;/em&gt; on a web shot and a low thud on the landing, and the exact same frames suddenly look like they cost ten times more.&lt;/p&gt;

&lt;p&gt;The trick is to treat audio as &lt;strong&gt;separate stems&lt;/strong&gt;, never one mixed track:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dialogue.&lt;/strong&gt; &lt;a href="https://elevenlabs.io" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt; for character voices. Generate it first, because the face needs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SFX and ambience.&lt;/strong&gt; &lt;a href="https://elevenlabs.io/docs/overview/capabilities/sound-effects" rel="noopener noreferrer"&gt;ElevenLabs sound effects&lt;/a&gt; turns a text description into a sound, up to 30 seconds per generation, with a seamless loop mode for rain and city hum, and 48 kHz WAV for one shots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Music.&lt;/strong&gt; Its own model, its own stem. &lt;a href="https://github.com/Stability-AI/stable-audio-tools" rel="noopener noreferrer"&gt;stable-audio-tools&lt;/a&gt; (~3.9k stars) is Stability's open code for their audio models if you want local control. Check the license of whatever you pick before it goes on a monetised channel.&lt;/p&gt;

&lt;p&gt;Separate stems are what make the edit programmable.&lt;/p&gt;

&lt;p&gt;Music too loud under the line? Change one number.&lt;/p&gt;

&lt;p&gt;Want the music to dip automatically whenever someone talks? That is FFmpeg's &lt;code&gt;sidechaincompress&lt;/code&gt; filter, keyed off the dialogue stem.&lt;/p&gt;

&lt;p&gt;None of that is possible once everything is baked into one WAV.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 7: render like a build system, assemble with FFmpeg
&lt;/h2&gt;

&lt;p&gt;Part two already turned the scene into a build artifact, so this layer is mostly "keep doing that, at movie scale".&lt;/p&gt;

&lt;p&gt;Three rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render headless.&lt;/strong&gt; &lt;code&gt;blender -b&lt;/code&gt; with no UI, driven by scripts, so a render is a command, not a person clicking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render frame sequences, not MP4s.&lt;/strong&gt; If Blender crashes on frame 1,400 of a movie file, you lose the movie. If it crashes on frame 1,400 of a PNG sequence, you lose one frame and resume. &lt;a href="https://docs.blender.org/manual/en/latest/render/output/animation.html" rel="noopener noreferrer"&gt;Blender's own docs&lt;/a&gt; recommend exactly this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draft at 360p, final once.&lt;/strong&gt; Part two's &lt;code&gt;--fast&lt;/code&gt; mode, with ray tracing, motion blur and extra samples switched off, roughly halved frame time. Everything is reviewed in drafts.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://github.com/FFmpeg/FFmpeg" rel="noopener noreferrer"&gt;FFmpeg&lt;/a&gt; (~64.9k stars, and it is only a mirror) glues frames and stems into a video:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# render shot 30 as a PNG sequence (flags before -a, Blender reads them in order)&lt;/span&gt;
blender &lt;span class="nt"&gt;-b&lt;/span&gt; build/sh030.blend &lt;span class="nt"&gt;-o&lt;/span&gt; //frames/sh030_#### &lt;span class="nt"&gt;-F&lt;/span&gt; PNG &lt;span class="nt"&gt;-s&lt;/span&gt; 1 &lt;span class="nt"&gt;-e&lt;/span&gt; 96 &lt;span class="nt"&gt;-a&lt;/span&gt;

&lt;span class="c"&gt;# frames + three stems -&amp;gt; one mp4, music ducked under the dialogue&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-framerate&lt;/span&gt; 30 &lt;span class="nt"&gt;-i&lt;/span&gt; frames/sh030_%04d.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-i&lt;/span&gt; audio/dialogue.wav &lt;span class="nt"&gt;-i&lt;/span&gt; audio/sfx.wav &lt;span class="nt"&gt;-i&lt;/span&gt; audio/music.wav &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[1:a]asplit[d][key];[3:a][key]sidechaincompress=threshold=0.05:ratio=8[m];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
[d][2:a][m]amix=inputs=3:normalize=0[a]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; 0:v &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[a]"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-crf&lt;/span&gt; 18 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac sh030.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;asplit&lt;/code&gt; makes two copies of the dialogue, one to hear and one to act as the key that pushes the music down.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;normalize=0&lt;/code&gt; stops &lt;code&gt;amix&lt;/code&gt; from quietly turning everything down to make room, which is a fun one to debug by ear.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-pix_fmt yuv420p&lt;/code&gt; is there because some players will happily show a black screen without it.&lt;/p&gt;

&lt;p&gt;I would still do the final colour pass and titles in &lt;a href="https://www.blackmagicdesign.com/products/davinciresolve" rel="noopener noreferrer"&gt;DaVinci Resolve&lt;/a&gt; for anything going on my channel, but a machine should be able to produce the rough cut alone.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 8: two reviewers, because one of them is blind
&lt;/h2&gt;

&lt;p&gt;The loop that made parts one and two work was Claude grabbing a screenshot, looking at it and fixing what it saw.&lt;/p&gt;

&lt;p&gt;That loop is great.&lt;/p&gt;

&lt;p&gt;It is also how a moonwalk got shipped.&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%2Fbrongda8agwpcbc2mpu5.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%2Fbrongda8agwpcbc2mpu5.png" alt="Two reviewers: Claude looking at a frame catches dark faces, wide lenses and bad framing; a 44-line script catches pops, floating feet and characters standing past the edge of a billboard" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vision models review a still picture.&lt;/p&gt;

&lt;p&gt;Motion bugs live &lt;em&gt;between&lt;/em&gt; the frames.&lt;/p&gt;

&lt;p&gt;The 22 cm pop in the middle of a crossfade, the single frame where Miles hovered at a cut, the stance wider than the ledge he stood on, none of those survive a look at frame 1,201.&lt;/p&gt;

&lt;p&gt;They all fail an assertion that measures where the feet are on every frame.&lt;/p&gt;

&lt;p&gt;So every shot gets both reviewers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eyes&lt;/strong&gt;: Claude looks at a contact sheet of draft frames and judges framing, lighting, mood and readability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asserts&lt;/strong&gt;: scripts measure contact, bounds, continuity at cuts and missing textures, and fail loudly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the asserts fail, nobody wastes time looking.&lt;/p&gt;
&lt;h2&gt;
  
  
  The missing piece: a director skill
&lt;/h2&gt;

&lt;p&gt;Everything above is a department.&lt;/p&gt;

&lt;p&gt;What turns departments into a studio is one skill that takes my paragraph and writes the plan everyone else works from.&lt;/p&gt;

&lt;p&gt;That plan should be data, not prose:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rooftop_siren&lt;/span&gt;
&lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="na"&gt;character&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;original_hero_v2&lt;/span&gt;       &lt;span class="c1"&gt;# not Miles, see above&lt;/span&gt;
&lt;span class="na"&gt;shots&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SH010&lt;/span&gt;
    &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rooftop_night_rain&lt;/span&gt;
    &lt;span class="na"&gt;camera&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;establishing_wide&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;push_in&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;motion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;clip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Standing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Idle"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;ambience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rain&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rooftop,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;distant&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;traffic,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wind"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SH020&lt;/span&gt;
    &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.5&lt;/span&gt;
    &lt;span class="na"&gt;camera&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;extreme_closeup&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;motion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;clip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Look&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Around"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;turn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;dialogue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;voice&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;hero&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hear&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;that?"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;face&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;lipsync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;rhubarb&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;blinks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;auto&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;look_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;siren&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SH030&lt;/span&gt;
    &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.2&lt;/span&gt;
    &lt;span class="na"&gt;camera&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tracking&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;low_angle&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;motion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;clip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Jump&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Down"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;speed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1.2&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;sfx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloth&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;whoosh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;thwip"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heavy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;landing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;concrete"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Look at what is in there.&lt;/p&gt;

&lt;p&gt;Every value is a skill name, a library lookup or a prompt for an audio model.&lt;/p&gt;

&lt;p&gt;Nothing asks the LLM to invent a joint angle, and nothing asks it to remember the whole movie at once.&lt;/p&gt;

&lt;p&gt;The director writes this once.&lt;/p&gt;

&lt;p&gt;Then each shot runs through the same loop, and a bad shot is re-run on its own instead of the whole film:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    S[Shot from the YAML] --&amp;gt; A[Generate dialogue&amp;lt;br/&amp;gt;and SFX]
    A --&amp;gt; L[Rhubarb&amp;lt;br/&amp;gt;mouth cues]
    S --&amp;gt; M[Fetch motion&amp;lt;br/&amp;gt;and set]
    L --&amp;gt; B[Build shot&amp;lt;br/&amp;gt;bpy scripts]
    M --&amp;gt; B
    B --&amp;gt; D[360p draft]
    D --&amp;gt; T{Asserts pass?}
    T --&amp;gt;|no| B
    T --&amp;gt;|yes| V{Claude likes&amp;lt;br/&amp;gt;the contact sheet?}
    V --&amp;gt;|no, fix camera or light| B
    V --&amp;gt;|yes| F[Final frames]
    F --&amp;gt; X[FFmpeg mux]

    classDef src fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef order fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a
    classDef fetch fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef build fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef out fill:#6ea8ff,stroke:#2f62c4,color:#1a1a1a

    class S src
    class A order
    class L,M fetch
    class B,D build
    class T,V decision
    class F,X out&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;It is CI for a movie.&lt;/p&gt;

&lt;p&gt;Each shot is a job, the YAML is the config, the asserts are the tests, and Claude is the reviewer who also writes the fixes.&lt;/p&gt;
&lt;h2&gt;
  
  
  The order I am building it in
&lt;/h2&gt;

&lt;p&gt;Not the order of the diagram, the order of the dependencies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bridge + skills.&lt;/strong&gt; One MCP server, a camera and lighting skill library. Without reliable shots, nothing else matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motion library.&lt;/strong&gt; Mixamo and CMU behind one retargeter, Mixamo Rig for IK, Rokoko Vision for the clips nobody has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio stems.&lt;/strong&gt; Voice, SFX, music, all separate. This comes &lt;em&gt;before&lt;/em&gt; the face, because the face reads the audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Face.&lt;/strong&gt; Rhubarb glue, procedural blinks and eye targets, then close-ups stop being scary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asserts.&lt;/strong&gt; Part two's checks, generalised to every shot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Director skill.&lt;/strong&gt; Only once every department works on its own. A director with no crew just writes very confident YAML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the question I will be honest about: will this make a Pixar short?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;What it can make is something I could not make at all a month ago: a 30 second scene with a character who moves like a person, talks with a mouth that matches the words, sounds like it happened somewhere, and was assembled by a machine while I wrote the next paragraph.&lt;/p&gt;

&lt;p&gt;That is a wrap for part three.&lt;/p&gt;

&lt;p&gt;If you have built any piece of this, especially the face, tell me what broke. I clearly have a lot of breaking ahead of me.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Blast-Radius Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is an AI code reviewer that scores every hunk of a diff by &lt;strong&gt;blast radius&lt;/strong&gt;: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;blast-radius-demo.mp4&lt;/span&gt;
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview's Blast Radius &amp;amp; Review Priority scoring, live in the diff viewer.&lt;/i&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;The exact math, not a black box&lt;/th&gt;

&lt;th&gt;Visualize blast radius at a glance&lt;/th&gt;

&lt;th&gt;Every factor that feeds the score&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-3.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-3.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-4.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-4.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-2.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-2.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

How does Blast Radius scoring work? (a more technical explanation)

&lt;p&gt;&lt;strong&gt;Here's the goal:&lt;/strong&gt;&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.&lt;/li&gt;

&lt;li&gt;A 300-line UI change in one file, fully covered by tests…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blender</category>
      <category>ai</category>
      <category>mcp</category>
      <category>claude</category>
    </item>
    <item>
      <title>Spider-Man Moonwalked Off a Billboard and Only My Test Suite Noticed</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Mon, 05 Oct 2026 20:26:54 +0000</pubDate>
      <link>https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7</link>
      <guid>https://dev.to/lovestaco/spider-man-moonwalked-off-a-billboard-and-only-my-test-suite-noticed-46k7</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Miles Morales moonwalked 6.5 metres past the edge of a billboard.&lt;/p&gt;

&lt;p&gt;Not on the billboard.&lt;/p&gt;

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

&lt;p&gt;On nothing.&lt;/p&gt;

&lt;p&gt;I had watched that shot more than once and did not notice.&lt;/p&gt;

&lt;p&gt;The viewport did not notice either.&lt;/p&gt;

&lt;p&gt;A 44 line Python script noticed, and it has been catching my mistakes ever since.&lt;/p&gt;

&lt;p&gt;This is part two of my accidental Blender education.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" class="crayons-story__hidden-navigation-link"&gt;I Made Spider-Man Swing Without Animating a Single Frame&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/lovestaco" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" alt="lovestaco profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/lovestaco" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Athreya aka Maneshwar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Athreya aka Maneshwar
                
                
              
              &lt;div id="story-author-preview-content-4793045" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/lovestaco" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1002302%2F9dbe5057-f6da-4c08-9b5d-37fe9d281476.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Athreya aka Maneshwar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" id="article-link-4793045"&gt;
          I Made Spider-Man Swing Without Animating a Single Frame
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/blender"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;blender&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;48&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              7&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            13 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;In &lt;a href="https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7"&gt;part one&lt;/a&gt; I went from never having opened Blender to rigging a Spider-Man model, pulling animations from Mixamo and retargeting old Carnegie Mellon mocap onto him.&lt;/p&gt;

&lt;p&gt;This one is about what happens next, when you try to turn that into an actual film and every mistake is suddenly a bug report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The film, and why it is really a build system
&lt;/h2&gt;

&lt;p&gt;The goal is a roughly 60 second explainer for LiveReview.&lt;/p&gt;

&lt;p&gt;A masked Miles web-swings through a city at sunset, and every "slide" is part of the street.&lt;/p&gt;

&lt;p&gt;A wall board, a rooftop billboard, an LED ticker, bus stop and subway lightboxes, a big street screen, and a runaway train for the finale.&lt;/p&gt;

&lt;p&gt;Here is the twist that shapes everything below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing in the scene is hand edited.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The whole thing is generated headless by Python scripts against Blender 5.2, rendered with EEVEE on a 4 GB GTX 1650.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;build/street.blend&lt;/code&gt; is not a file I open and tweak.&lt;/p&gt;

&lt;p&gt;It is a build artifact, like a &lt;code&gt;dist/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;So every fix is a code change plus a rebuild, and the workflow ended up looking a lot more like CI than like art class:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    S[Python scripts] --&amp;gt;|blender -b| B[street.blend]
    B --&amp;gt; C{support check clean?}
    C --&amp;gt;|no| S
    C --&amp;gt;|yes| D[--fast draft render]
    D --&amp;gt; T[review copy with timecode]
    T --&amp;gt; R{looks right?}
    R --&amp;gt;|no, f1201 is off| S
    R --&amp;gt;|yes| G[git commit]
    G --&amp;gt; F[full quality render]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef code fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef build fill:#6ea8ff,stroke:#2f62c4,color:#1a1a1a
    classDef render fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a
    classDef done fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a

    class C,R decision
    class S,G code
    class B build
    class D,T render
    class F done&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Almost every lesson in this post fell out of that loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Can't you just use the GPU to render faster?"
&lt;/h2&gt;

&lt;p&gt;This was my first question once renders started taking a while.&lt;/p&gt;

&lt;p&gt;The answer was a little embarrassing.&lt;/p&gt;

&lt;p&gt;It already was.&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%2Frmroyuhttb83qxlmfs7j.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%2Frmroyuhttb83qxlmfs7j.png" alt="Always Has Been meme: me asking if it's been on the GPU, EEVEE replying that it always has been"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.blender.org/manual/en/latest/render/eevee/index.html" rel="noopener noreferrer"&gt;EEVEE&lt;/a&gt; is a GPU rasterizer, basically a game engine renderer.&lt;/p&gt;

&lt;p&gt;While it rendered, the card sat at about 80% utilisation and 2.2 GB of VRAM.&lt;/p&gt;

&lt;p&gt;There was no idle GPU waiting to be switched on.&lt;/p&gt;

&lt;p&gt;And the other renderer, &lt;a href="https://docs.blender.org/manual/en/latest/render/cycles/index.html" rel="noopener noreferrer"&gt;Cycles&lt;/a&gt;, is a path tracer, which on a GTX 1650 would have been &lt;em&gt;slower&lt;/em&gt;, not faster.&lt;/p&gt;

&lt;p&gt;So the real question was never "CPU or GPU".&lt;/p&gt;

&lt;p&gt;It was "what is each frame paying for?"&lt;/p&gt;

&lt;p&gt;Three settings turned out to dominate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;th&gt;Draft value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screen-space ray tracing&lt;/td&gt;
&lt;td&gt;reflections on the wet street, the single biggest per-frame cost&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Motion blur&lt;/td&gt;
&lt;td&gt;renders extra time steps for every frame&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Samples&lt;/td&gt;
&lt;td&gt;anti-aliasing and soft shadows, linear in render time&lt;/td&gt;
&lt;td&gt;8 instead of 16 to 24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fswk7oy0xhwfcxeins4fe.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%2Fswk7oy0xhwfcxeins4fe.png" alt="Diagram: the GTX 1650 already at about 80% busy with 2.2 of 4 GB VRAM used, the three expensive features each frame pays for being switched off, and seconds per frame dropping from about 0.6 to about 0.33, taking the full film from about 18 minutes to about 10"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the render script grew a &lt;code&gt;--fast&lt;/code&gt; flag that flips exactly those three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--fast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;          &lt;span class="c1"&gt;# draft: no ray tracing, no motion blur, 8 samples
&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eevee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taa_render_samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eevee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;use_raytracing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eevee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use_raytracing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use_motion_blur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At 360p that took a frame from about 0.6 s to about 0.33 s.&lt;/p&gt;

&lt;p&gt;Roughly 2x.&lt;/p&gt;

&lt;p&gt;The full 58 second film is 1,760 frames, so a review render went from around 18 minutes to around 10.&lt;/p&gt;

&lt;p&gt;What you give up is real but harmless.&lt;/p&gt;

&lt;p&gt;The street looks flatter without the ray-traced reflections, and fast motion looks crisper without blur.&lt;/p&gt;

&lt;p&gt;Nobody reviewing "does Miles land on the ledge" cares about either.&lt;/p&gt;

&lt;p&gt;The finals still get everything.&lt;/p&gt;

&lt;p&gt;A few other levers came along for the ride:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 270p option&lt;/strong&gt; (480x270) for the quickest "is the blocking right" passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact sheets.&lt;/strong&gt; A strip of stills at 25% resolution and 8 samples, before committing to any video at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial re-renders.&lt;/strong&gt; &lt;code&gt;05_render.py -- full 33 &amp;lt;from_frame&amp;gt;&lt;/code&gt; re-renders only from a given frame onward and reuses the earlier frames, for when only the ending changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson is the same one you learn profiling a slow API.&lt;/p&gt;

&lt;p&gt;Before you ask for a bigger machine, find out what the work actually is.&lt;/p&gt;

&lt;p&gt;Most of the time you are paying for something nobody looks at.&lt;/p&gt;
&lt;h2&gt;
  
  
  "How do I go back? Is there a snapshot button?"
&lt;/h2&gt;

&lt;p&gt;Blender has more undo than you would expect, in layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ctrl+Z and Ctrl+Shift+Z&lt;/strong&gt; for in-session undo and redo, plus &lt;em&gt;Edit → Undo History&lt;/em&gt; to jump back several steps at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File → Revert&lt;/strong&gt; to throw away unsaved changes and reload from disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.blend1&lt;/code&gt; backups.&lt;/strong&gt; Every save keeps the previous version next to the file, and &lt;em&gt;Preferences → Save &amp;amp; Load → Save Versions&lt;/em&gt; sets how many (&lt;a href="https://docs.blender.org/manual/en/latest/editors/preferences/save_load.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File → Recover → Auto Save&lt;/strong&gt; for when Blender crashes, which it will.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All useful, and all beside the point here.&lt;/p&gt;

&lt;p&gt;Because when the &lt;code&gt;.blend&lt;/code&gt; is generated, it is not the source of truth.&lt;/p&gt;

&lt;p&gt;The scripts are.&lt;/p&gt;

&lt;p&gt;So the real snapshot button is git:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; miles/scripts/04_street.py    &lt;span class="c"&gt;# pick a commit&lt;/span&gt;
git stash                                          &lt;span class="c"&gt;# park current work&lt;/span&gt;
git checkout &amp;lt;commit&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; miles/                    &lt;span class="c"&gt;# that version's scripts and assets&lt;/span&gt;
blender &lt;span class="nt"&gt;-b&lt;/span&gt; build/character.blend &lt;span class="nt"&gt;--python&lt;/span&gt; scripts/04_street.py   &lt;span class="c"&gt;# rebuild that scene&lt;/span&gt;
git checkout HEAD &lt;span class="nt"&gt;--&lt;/span&gt; miles/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git stash pop       &lt;span class="c"&gt;# and come back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If the scene is generated from code, version the code, not the binary.&lt;/p&gt;

&lt;p&gt;I commit after every review round now, so any version I ever showed someone can be rebuilt exactly.&lt;/p&gt;

&lt;p&gt;The old rendered videos stick around too (&lt;code&gt;street_v1&lt;/code&gt;, &lt;code&gt;v4a&lt;/code&gt; and friends), because sometimes you just want to play two versions side by side without rebuilding anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  A reader comment that found a real bug
&lt;/h2&gt;

&lt;p&gt;Part one got &lt;a href="https://dev.to/lewisywliu/comment/3gf6d"&gt;a comment&lt;/a&gt; from &lt;a href="https://dev.to/lewisywliu"&gt;@lewisywliu&lt;/a&gt; that was genuinely better than some docs I have read.&lt;/p&gt;

&lt;p&gt;Two tips, paraphrased:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BVH files define their own euler rotation order per joint, and the importer does not always match it, so wrists and feet can end up subtly wrong even when the hips look fine. If a pose reads almost right but the hands are off, check euler order before touching offsets.&lt;/p&gt;

&lt;p&gt;Consecutive clips with different hip heights, like land to idle or crouch to stand, need the vertical offset blended too, otherwise the character floats or sinks for a few frames while the horizontal path snaps correctly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first one turned out to be a near miss.&lt;/p&gt;

&lt;p&gt;The second one was a bug I already had and had not seen.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tip 1: euler order, or why the hands go wrong first
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://research.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/BVH.html" rel="noopener noreferrer"&gt;BVH file&lt;/a&gt;, which is what the &lt;a href="http://mocap.cs.cmu.edu/" rel="noopener noreferrer"&gt;CMU mocap library&lt;/a&gt; ships as, stores each joint's rotation as three angles.&lt;/p&gt;

&lt;p&gt;But it also stores the &lt;em&gt;order&lt;/em&gt; those three rotations are applied in, and that order is per joint.&lt;/p&gt;

&lt;p&gt;One joint can say Z then X then Y, the next can say Y then X then Z.&lt;/p&gt;

&lt;p&gt;That matters because &lt;a href="https://en.wikipedia.org/wiki/Euler_angles" rel="noopener noreferrer"&gt;rotations do not commute&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Rotate 30 degrees around Z and then 45 around X, and you end up facing somewhere different from 45 around X and then 30 around Z.&lt;/p&gt;

&lt;p&gt;Same numbers, different pose.&lt;/p&gt;

&lt;p&gt;Now put that in a skeleton.&lt;/p&gt;

&lt;p&gt;Every bone inherits its parent's rotation, so a small error at the shoulder moves the elbow, which moves the wrist, which moves the hand.&lt;/p&gt;

&lt;p&gt;The error compounds down the chain.&lt;/p&gt;

&lt;p&gt;The hips are at the top of the tree, so they look perfect.&lt;/p&gt;

&lt;p&gt;The hands are at the bottom, so they are the first thing that looks "almost right but weird".&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%2Fgmljhaqv6wxng70eebcn.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%2Fgmljhaqv6wxng70eebcn.png" alt="Diagram: the same bone chain from hips to hand drawn twice, once with each joint's own rotation order and once with one fixed order for every joint, identical at the hips and spine and drifting further apart at each joint until the hand is visibly off"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I checked the importer call, half expecting the worst.&lt;/p&gt;

&lt;p&gt;It already passed &lt;code&gt;rotate_mode="NATIVE"&lt;/code&gt; to &lt;a href="https://docs.blender.org/api/current/bpy.ops.import_anim.html" rel="noopener noreferrer"&gt;&lt;code&gt;bpy.ops.import_anim.bvh&lt;/code&gt;&lt;/a&gt;, which means "use each joint's own order from the file".&lt;/p&gt;

&lt;p&gt;So we were safe.&lt;/p&gt;

&lt;p&gt;But it is now the first thing on my list if a CMU clip ever shows odd wrists or ankles, before anyone reaches for an offset hack.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tip 2: hip height across a blend, which bit me
&lt;/h3&gt;

&lt;p&gt;Quick refresher on how the clips are chained.&lt;/p&gt;

&lt;p&gt;Each clip is mostly "in place", so to make Miles actually travel, each clip's movement is added on top of wherever the last one ended.&lt;/p&gt;

&lt;p&gt;A running sum.&lt;/p&gt;

&lt;p&gt;That is &lt;a href="https://docs.unity3d.com/Manual/RootMotion.html" rel="noopener noreferrer"&gt;root motion&lt;/a&gt;, the same idea game engines use.&lt;/p&gt;

&lt;p&gt;Everyone remembers to blend the horizontal part of that path.&lt;/p&gt;

&lt;p&gt;The second tip was that the &lt;em&gt;vertical&lt;/em&gt; part needs blending too.&lt;/p&gt;

&lt;p&gt;(Game engines usually call that Y. Blender is Z-up, because of course it is.)&lt;/p&gt;

&lt;p&gt;On flat ground I never saw a problem.&lt;/p&gt;

&lt;p&gt;On raised surfaces I had exactly this bug, and I only found it once I measured it.&lt;/p&gt;

&lt;p&gt;The probe was simple: on every frame through each blend, take the gap between the lowest toe and the surface under him.&lt;/p&gt;

&lt;p&gt;The target is about +5 cm, because the toe bone sits a little above the sole of the shoe.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Blend, on a raised surface&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crouch → web shot, on a board's top ledge&lt;/td&gt;
&lt;td&gt;sinks ~10 cm for 3 frames&lt;/td&gt;
&lt;td&gt;+0.03 to +0.07 m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crouch → moonwalk, on a billboard catwalk&lt;/td&gt;
&lt;td&gt;pops up 22 cm, then settles&lt;/td&gt;
&lt;td&gt;+0.02 to +0.10 m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crouch → taunt, on a rooftop&lt;/td&gt;
&lt;td&gt;drifts up 10 cm&lt;/td&gt;
&lt;td&gt;+0.04 to +0.07 m&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 22 cm pop is a Spider-Man who briefly levitates for no reason.&lt;/p&gt;

&lt;p&gt;Which, to be fair, is on brand, but not what I wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first fix did exactly what the tip says, and it did nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I eased the root height smoothly across the blend window instead of letting it step.&lt;/p&gt;

&lt;p&gt;The numbers barely moved.&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%2Fll4jeo91cy98mi58r7f7.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%2Fll4jeo91cy98mi58r7f7.png" alt="Guy hammering nails into sand meme: me, hammering " height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The measurement showed why.&lt;/p&gt;

&lt;p&gt;The hips were not the problem.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;crossfaded pose&lt;/em&gt; was.&lt;/p&gt;

&lt;p&gt;Halfway between a crouch and a standing pose, the blended legs are half extended, so the feet rise on their own, no matter how gently the hips move.&lt;/p&gt;

&lt;p&gt;A smooth curve for the hip height cannot follow what the legs are doing.&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%2Fbp8x6ur73ig39g6v80ky.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%2Fbp8x6ur73ig39g6v80ky.png" alt="Diagram: three stick figures, crouch, mid-blend and standing, where the mid-blend figure's feet float above the ledge, above a graph of toe height where easing the hips still pops up 22 cm while per-frame contact stays between 2 and 10 cm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked was to stop predicting and start measuring.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every frame in a raised shot, find the lowest toe, and move the root so that toe sits a fixed distance above the surface.&lt;/p&gt;

&lt;p&gt;Then smooth it lightly so it does not jitter.&lt;/p&gt;

&lt;p&gt;This is the real function, trimmed a little:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Keep the lowest toe `toe` above a surface at height `floor` for every frame.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;fc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fcurve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frame_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matrix_world&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;
                       &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mixamorig:LeftToeBase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mixamorig:RightToeBase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;corr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;toe&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# a hop in the clip stays a hop
&lt;/span&gt;    &lt;span class="n"&gt;sm&lt;/span&gt; &lt;span class="o"&gt;=&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;corr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&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;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;smooth&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="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;corr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&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;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;smooth&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;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;corr&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;                                   &lt;span class="c1"&gt;# average over ±2 frames
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                     &lt;span class="c1"&gt;# remove back to front: indices stay valid
&lt;/span&gt;        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyframe_points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;kp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;co&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="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyframe_points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyframe_points&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;anim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# a key on EVERY frame
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That &lt;code&gt;limit=0.35&lt;/code&gt; is load bearing.&lt;/p&gt;

&lt;p&gt;Without a cap, one clip that genuinely hops at the end got "corrected" by &lt;strong&gt;8 metres&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The function saw feet in the air, decided the floor must be way off, and dragged the whole character to meet them.&lt;/p&gt;

&lt;p&gt;The ±35 cm clamp means a real hop stays a hop.&lt;/p&gt;

&lt;p&gt;And yes, that "remove back to front" comment is a scar.&lt;/p&gt;

&lt;p&gt;Which brings us to the bugs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three bugs the measurement dug up
&lt;/h2&gt;

&lt;p&gt;Once there was a probe printing a number per frame, the number started pointing at things I would never have seen in a viewport.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Miles, 18 metres up, for exactly one frame
&lt;/h3&gt;

&lt;p&gt;An earlier floor-lock pass keyed the root height on every &lt;em&gt;other&lt;/em&gt; frame, with linear interpolation in between.&lt;/p&gt;

&lt;p&gt;That is a perfectly reasonable thing to do inside one shot.&lt;/p&gt;

&lt;p&gt;It is a terrible thing to do across a hard cut.&lt;/p&gt;

&lt;p&gt;At a cut from a rooftop to the street, the frame between two keys landed halfway between two shots.&lt;/p&gt;

&lt;p&gt;Linear interpolation averaged "rooftop" and "street" and put Miles about 18 m up, in the sky above the street, for one frame.&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%2Fbq5e0vnim7x9xe9mjwq2.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%2Fbq5e0vnim7x9xe9mjwq2.png" alt="Diagram: root height across a rooftop to street cut. With a key every 2nd frame and linear interpolation, frame 1037 lands halfway between the two shots. With a key on every frame, the height steps cleanly at the cut"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At 30 fps that is a 33 millisecond glitch.&lt;/p&gt;

&lt;p&gt;You would never catch it playing the video.&lt;/p&gt;

&lt;p&gt;The probe flagged it instantly.&lt;/p&gt;

&lt;p&gt;The fix: key every frame inside these ranges, so there is no in-between frame left to interpolate.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Hard cuts on fractional frames
&lt;/h3&gt;

&lt;p&gt;Clips are scheduled in float frames, because blend lengths and speeds are computed.&lt;/p&gt;

&lt;p&gt;So a "hard cut" could start at frame 1037.4.&lt;/p&gt;

&lt;p&gt;Blender renders whole frames, which meant frame 1037 showed the old pose at the new position.&lt;/p&gt;

&lt;p&gt;Half of one shot glued to half of another.&lt;/p&gt;

&lt;p&gt;The fix: hard cuts snap to a whole frame with &lt;code&gt;math.ceil&lt;/code&gt;, and a shot's last frame is &lt;code&gt;ceil(end) - 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the animation version of an off-by-one, and it was hiding inside a float.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Deleting keyframes while iterating over them
&lt;/h3&gt;

&lt;p&gt;This one is a classic, and I still walked straight into it.&lt;/p&gt;

&lt;p&gt;The code collected references to the keyframes it wanted to replace, then looped over them calling &lt;code&gt;keyframe_points.remove(kp, fast=True)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But removing a keyframe shifts every index after it.&lt;/p&gt;

&lt;p&gt;So the next reference in the list now pointed at its &lt;em&gt;neighbour&lt;/em&gt;, and the loop silently deleted the wrong key.&lt;/p&gt;

&lt;p&gt;The first frame of two shots ended up with no height key at all.&lt;/p&gt;

&lt;p&gt;It is the same bug as removing items from a Python list inside &lt;code&gt;for x in items:&lt;/code&gt;, just wearing a 3D costume.&lt;/p&gt;

&lt;p&gt;The fix is the one in the snippet above: collect indices, then remove from the back.&lt;/p&gt;

&lt;p&gt;The whole lesson of this section is not about any one of those bugs.&lt;/p&gt;

&lt;p&gt;It is that the first fix looked right in theory and did nothing in practice, and only a number per frame could tell me that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not eyeball root motion. Measure it.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The probe became a test suite
&lt;/h2&gt;

&lt;p&gt;Once the foot probe existed, it seemed silly to only run it when I suspected a problem.&lt;/p&gt;

&lt;p&gt;So it became &lt;code&gt;scripts/check_support.py&lt;/code&gt;, a permanent pre-render check.&lt;/p&gt;

&lt;p&gt;It is about 40 lines and it answers one question, sampling every third frame of the film: is there something under his feet?&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    F[Next frame] --&amp;gt; A{airborne clip or upside down?}
    A --&amp;gt;|yes, swing, hang, flip| F
    A --&amp;gt;|no| T[Find the lowest toe]
    T --&amp;gt; R[Ray cast straight down]
    R --&amp;gt; M{hit Miles, a web, a spider or cash?}
    M --&amp;gt;|yes, step past it| R
    M --&amp;gt;|no| G{gap over 25 cm or sinking?}
    G --&amp;gt;|no| F
    G --&amp;gt;|yes| B[Flag the frame]
    B --&amp;gt; F

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef step fill:#6ea8ff,stroke:#2f62c4,color:#1a1a1a
    classDef bad fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a

    class A,M,G decision
    class T,R step
    class B bad
    class F start&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;It uses Blender's &lt;a href="https://docs.blender.org/api/current/bpy.types.Scene.html#bpy.types.Scene.ray_cast" rel="noopener noreferrer"&gt;&lt;code&gt;scene.ray_cast&lt;/code&gt;&lt;/a&gt;, ignores anything that is part of the character or the props (a web is not a floor, sadly), skips the moments where he is supposed to be in the air, and groups flagged frames into runs so the output reads like a test report.&lt;/p&gt;

&lt;p&gt;I run it before every render, the same way you would not deploy without running the tests.&lt;/p&gt;

&lt;p&gt;And it caught things a visual check flat out missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moonwalking on thin air.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The opening of this post.&lt;/p&gt;

&lt;p&gt;Miles moonwalked 6.5 m past the edge of a billboard's frame, and, for bonus points, in the opposite direction to the customers walking off the screen behind him.&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%2Fj6f0jq1e751fxmvlt7kt.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%2Fj6f0jq1e751fxmvlt7kt.png" alt="Spirit Halloween costume meme: "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix had two parts.&lt;/p&gt;

&lt;p&gt;A steel catwalk under the screen, because real billboards have one for maintenance anyway.&lt;/p&gt;

&lt;p&gt;And the moonwalk now goes &lt;em&gt;with&lt;/em&gt; the customers, which is a better joke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 40 cm stance on a 25 cm board.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;He stood on top of a wall board whose top was 25 cm deep.&lt;/p&gt;

&lt;p&gt;His stance was 40 cm.&lt;/p&gt;

&lt;p&gt;One foot was hanging off the front, standing on air.&lt;/p&gt;

&lt;p&gt;The fix was a 1.25 m service ledge on top of the board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A turn that swung him 2 m off a ledge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was my favourite, because the cause was pure geometry.&lt;/p&gt;

&lt;p&gt;In-place clips leave the rig's root pivot nowhere near the hips.&lt;/p&gt;

&lt;p&gt;In this case it was about 4 m away.&lt;/p&gt;

&lt;p&gt;So when a blend rotated the root to turn him 150 degrees, his whole body orbited a point 4 m away, like a door swinging on a hinge in the next room.&lt;/p&gt;

&lt;p&gt;Mid-turn, his hips were 2 m off the ledge.&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%2Filciblzm8hoxk64l5tvj.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%2Filciblzm8hoxk64l5tvj.png" alt="Diagram: top-down view of a narrow ledge. Before: rotating the root about a pivot 4 m away swings the hips in an arc about 2 m off the ledge. After: keying the root every frame keeps the hips on a straight line while the body turns in place"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix was to key the root per frame during a turning blend, so the hips travel in a straight line while the body rotates around them.&lt;/p&gt;

&lt;p&gt;A turn is a rotation about &lt;em&gt;some&lt;/em&gt; point.&lt;/p&gt;

&lt;p&gt;It is worth checking which one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Small workflow wins
&lt;/h2&gt;

&lt;p&gt;None of these deserve a section, but all of them saved real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Burned-in review copies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every render also writes a &lt;code&gt;&amp;lt;name&amp;gt;_tc.mp4&lt;/code&gt; with the Blender frame number and "time / total" stamped in the corner.&lt;/p&gt;

&lt;p&gt;When someone says "f1201 looks off", that maps straight to a frame in the scene.&lt;/p&gt;

&lt;p&gt;No more "uh, around the middle, after the bus thing".&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Risk-Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is built for engineering teams that ship often, but also care about keeping production stable for customers. It approves some 40-50% of low-risk changes automatically, and it gives your team the tools to handle the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand risky changes faster.&lt;/li&gt;
&lt;li&gt;Fix problems in those changes, with human attention directed to the work that needs it.&lt;/li&gt;
&lt;li&gt;Enforce your engineering standards.&lt;/li&gt;
&lt;li&gt;Learn from production outcomes, such as incidents, rollbacks, and queue wait times.&lt;/li&gt;
&lt;li&gt;Improve the review process over time and make it more efficient.&lt;/li&gt;
&lt;li&gt;Connect to the major engineering tools that your team already uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Auto-Approve: Ship the Safe 50% Without a Human&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Auto-Approve Upto 50% of Your Pull Requests, Safely.&lt;/strong&gt; Risk scoring decides which MRs/PRs are safe enough to ship without a human, so your team only spends review time on risky and critical changes.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;demo.mp4&lt;/span&gt;
    
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview auto-approving a low-risk change while&lt;/i&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blender</category>
      <category>python</category>
      <category>gamedev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I Made Spider-Man Swing Without Animating a Single Frame</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Sat, 03 Oct 2026 18:05:25 +0000</pubDate>
      <link>https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7</link>
      <guid>https://dev.to/lovestaco/i-made-spider-man-swing-without-animating-a-single-frame-blender-rigging-and-mcp-14f7</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yes, that is Miles Morales.&lt;/p&gt;

&lt;p&gt;He drops in on a web, lands a superhero crouch, fires a web off screen, and yanks a giant DEV board into frame.&lt;/p&gt;

&lt;p&gt;I did not animate a single frame of that by hand.&lt;/p&gt;

&lt;p&gt;I also had never opened Blender before this week.&lt;/p&gt;

&lt;p&gt;This post is the story of how that happened, and the surprising amount of computer science hiding inside a "simple" 3D character.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Blender, of all things
&lt;/h2&gt;

&lt;p&gt;I was trying to make a video.&lt;/p&gt;

&lt;p&gt;Not a screen recording, an actual video with something moving in it.&lt;/p&gt;

&lt;p&gt;My first stop was &lt;a href="https://github.com/heygen-com/hyperframes" rel="noopener noreferrer"&gt;HyperFrames&lt;/a&gt;, which lets you write a video as HTML and render it out.&lt;/p&gt;

&lt;p&gt;It is genuinely great at motion graphics.&lt;/p&gt;

&lt;p&gt;Text that slides in, logos that pop, charts that draw themselves, all of that is lovely.&lt;/p&gt;

&lt;p&gt;But the moment I wanted a &lt;em&gt;character&lt;/em&gt; to do something, it hit a wall.&lt;/p&gt;

&lt;p&gt;HTML can move rectangles around beautifully.&lt;/p&gt;

&lt;p&gt;It cannot make a guy swing on a web.&lt;/p&gt;

&lt;p&gt;So the next question was obvious, if a little scary: what do actual animators use?&lt;/p&gt;

&lt;p&gt;The answer, for free and open source, is &lt;a href="https://www.blender.org/" rel="noopener noreferrer"&gt;Blender&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And Blender has an MCP server, which meant I would not be learning it alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP that drove the whole thing
&lt;/h2&gt;

&lt;p&gt;The thing that made this possible for a total beginner is &lt;a href="https://github.com/ahujasid/mcp-for-blender" rel="noopener noreferrer"&gt;blender-mcp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It connects Blender to an LLM client like Claude Code, so instead of hunting through 400 menus, you say "load this model and relink its textures" and it happens.&lt;/p&gt;

&lt;p&gt;The architecture is worth understanding, because it is simpler than it sounds.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    ME[Me typing a prompt] --&amp;gt; CC[Claude Code]
    CC --&amp;gt;|MCP over stdio| SRV[blender-mcp server]
    SRV --&amp;gt;|JSON over TCP socket| ADDON[Blender add-on]
    ADDON --&amp;gt;|runs Python| BPY[bpy API]
    BPY --&amp;gt; SCENE[Live Blender scene]
    SCENE --&amp;gt;|viewport screenshot| CC

    classDef human fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef agent fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef bridge fill:#6ea8ff,stroke:#2f62c4,color:#1a1a1a
    classDef blender fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a

    class ME human
    class CC agent
    class SRV,ADDON bridge
    class BPY,SCENE blender&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;There are two halves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An add-on inside Blender&lt;/strong&gt; opens a small socket server and waits for commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An MCP server outside Blender&lt;/strong&gt; speaks the Model Context Protocol to the LLM on one side, and forwards JSON commands down that socket on the other.&lt;/p&gt;

&lt;p&gt;The most powerful tool it exposes is &lt;code&gt;execute_blender_code&lt;/code&gt;, which runs arbitrary Python inside Blender through &lt;code&gt;bpy&lt;/code&gt;, Blender's Python API.&lt;/p&gt;

&lt;p&gt;Every button in Blender is just a thin skin over &lt;code&gt;bpy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So an agent that can write &lt;code&gt;bpy&lt;/code&gt; can, in principle, press every button.&lt;/p&gt;

&lt;p&gt;The loop that makes it work is the screenshot tool coming back the other way.&lt;/p&gt;

&lt;p&gt;The agent changes something, grabs the viewport, &lt;em&gt;looks at it&lt;/em&gt;, and decides what to fix next.&lt;/p&gt;

&lt;p&gt;That is the same edit, run, inspect loop you use on code, just pointed at a 3D scene.&lt;/p&gt;

&lt;p&gt;One honest warning: that tool is arbitrary code execution by design. Treat it like you would treat any local automation with a shell, and do not point it at a machine full of secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: get a character
&lt;/h2&gt;

&lt;p&gt;I am not a 3D modeler, and I was not about to become one by Friday.&lt;/p&gt;

&lt;p&gt;So I grabbed a ready made Miles Morales model, the Spider-Verse Spider-Man, from &lt;a href="https://sketchfab.com/" rel="noopener noreferrer"&gt;Sketchfab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9cguod6ys25tyg6bluw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9cguod6ys25tyg6bluw.gif" alt="Loading Miles into Blender for the first time" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even "just load it" had lessons in it.&lt;/p&gt;

&lt;p&gt;The file came with &lt;strong&gt;four&lt;/strong&gt; copies of Miles standing in a row: masked and unmasked, posed and T-posed.&lt;/p&gt;

&lt;p&gt;The textures pointed at the original author's Windows folders, like &lt;code&gt;C:\Users\someone\Desktop\...&lt;/code&gt;, so the agent had to relink them to the files in the zip.&lt;/p&gt;

&lt;p&gt;And the zip was missing the normal and specular maps.&lt;/p&gt;

&lt;p&gt;In Blender, a texture node pointing at a missing file renders &lt;strong&gt;hot pink&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is the 3D equivalent of a 404, and it is very, very visible.&lt;/p&gt;

&lt;p&gt;The fix was to disconnect those slots, at the cost of some surface detail on the suit.&lt;/p&gt;

&lt;p&gt;Then I opened the unmasked Miles and his face looked like a horror movie, covered in white lines and dots.&lt;/p&gt;

&lt;p&gt;Turns out the face was fine.&lt;/p&gt;

&lt;p&gt;That model ships with a &lt;strong&gt;143 bone face rig&lt;/strong&gt; (eyelids, brows, lips, jaw, cheeks, even a tongue), and the armature was set to "draw in front", so every one of those bones was painted on top of his skin.&lt;/p&gt;

&lt;p&gt;One checkbox later, he was handsome again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept that unlocks everything: rigging
&lt;/h2&gt;

&lt;p&gt;Here is where I learned the word that the rest of this post hangs on.&lt;/p&gt;

&lt;p&gt;A 3D character is, at its core, a &lt;strong&gt;mesh&lt;/strong&gt;: a big list of vertices (Miles has about 27k faces) glued into triangles.&lt;/p&gt;

&lt;p&gt;A mesh is a statue.&lt;/p&gt;

&lt;p&gt;You cannot make a statue wave.&lt;/p&gt;

&lt;p&gt;To move it, you need three more things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An armature&lt;/strong&gt;, which is a skeleton.&lt;/p&gt;

&lt;p&gt;It is a tree of &lt;strong&gt;bones&lt;/strong&gt;, each one a parent of the next: hips to spine to chest to shoulder to upper arm to forearm to hand to fingers.&lt;/p&gt;

&lt;p&gt;Rotate the shoulder and everything below it in the tree comes along for the ride, exactly like a transform hierarchy in any game engine or a nested &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with a CSS &lt;code&gt;transform&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skin weights&lt;/strong&gt;, which bind the mesh to the skeleton.&lt;/p&gt;

&lt;p&gt;Every vertex stores a small table saying how much each bone influences it.&lt;/p&gt;

&lt;p&gt;A vertex on the forearm might be 100% &lt;code&gt;forearm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A vertex right at the elbow might be 50% &lt;code&gt;upper_arm&lt;/code&gt; and 50% &lt;code&gt;forearm&lt;/code&gt;, which is what lets the elbow bend smoothly instead of folding like a paper straw.&lt;/p&gt;

&lt;p&gt;The math is called linear blend skinning, and it is just a weighted average of matrices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final_position(v) = Σ  weight(v, bone) × bone_matrix × rest_position(v)
                   bones
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;A rest pose&lt;/strong&gt;, which is the reference everything is measured from.&lt;/p&gt;

&lt;p&gt;That is why every character you download stands in a &lt;strong&gt;T-pose&lt;/strong&gt;, arms straight out.&lt;/p&gt;

&lt;p&gt;It is not a dance move.&lt;/p&gt;

&lt;p&gt;It is the zero point, the &lt;code&gt;git init&lt;/code&gt; of the body, and every animation is stored as rotations &lt;em&gt;relative&lt;/em&gt; to it.&lt;/p&gt;

&lt;p&gt;Put those together and you have a &lt;strong&gt;rig&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And then the naive plan fell apart, because the model's rig had bones but no &lt;em&gt;controls&lt;/em&gt;.&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%2Fnrn9eh1i6wtloviyls0p.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%2Fnrn9eh1i6wtloviyls0p.png" alt="Gru's plan meme: download a Spider-Man model, make him dance, he has bones but no rig" width="360" height="230"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Forward vs inverse kinematics
&lt;/h2&gt;

&lt;p&gt;I had seen YouTube videos where an animator grabs a character's hips, drags them down, and the knees bend while the feet stay glued to the floor.&lt;/p&gt;

&lt;p&gt;When I dragged Miles's hips, his whole body floated down through the floor like a ghost.&lt;/p&gt;

&lt;p&gt;The difference is one of the nicest ideas in this whole field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward kinematics (FK)&lt;/strong&gt; is what a raw skeleton does.&lt;/p&gt;

&lt;p&gt;You set the angle of every joint, and the hand ends up wherever the chain of rotations puts it.&lt;/p&gt;

&lt;p&gt;Easy to compute, miserable to animate. Want the hand on the doorknob? Good luck guessing the shoulder, elbow and wrist angles that land it there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inverse kinematics (IK)&lt;/strong&gt; flips the question.&lt;/p&gt;

&lt;p&gt;You say where the hand (or foot) should be, and a solver works backwards to find joint angles that put it there.&lt;/p&gt;

&lt;p&gt;It is literally an optimisation problem solved every frame, and Blender ships &lt;a href="https://docs.blender.org/manual/en/latest/animation/armatures/posing/bone_constraints/inverse_kinematics/introduction.html" rel="noopener noreferrer"&gt;an IK constraint&lt;/a&gt; that does it for you.&lt;/p&gt;

&lt;p&gt;Pin the feet with IK targets, drag the hips down, and the solver &lt;em&gt;has&lt;/em&gt; to bend the knees because the feet are not allowed to move.&lt;/p&gt;

&lt;p&gt;That is the magic in those YouTube videos.&lt;/p&gt;

&lt;p&gt;It is not magic, it is a constraint solver.&lt;/p&gt;

&lt;p&gt;Building a full IK control rig by hand is a real skill though.&lt;/p&gt;

&lt;p&gt;And I wanted Miles doing backflips this week, not next quarter.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mixamo: rigging as a service
&lt;/h2&gt;

&lt;p&gt;Enter &lt;a href="https://www.mixamo.com" rel="noopener noreferrer"&gt;Mixamo&lt;/a&gt;, Adobe's free auto-rigger and animation library.&lt;/p&gt;

&lt;p&gt;The flow is: export your character as an FBX in T-pose, upload it, and Mixamo builds a skeleton and skin weights for you.&lt;/p&gt;

&lt;p&gt;The only thing it asks of you is to place a few markers.&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%2Femmbgwovltrnpu7dria0.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%2Femmbgwovltrnpu7dria0.png" alt="Mixamo's auto-rigger asking for markers on the chin, wrists, elbows, knees and groin" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chin, wrists, elbows, knees, groin.&lt;/p&gt;

&lt;p&gt;Five landmarks, with symmetry on, so really just three clicks per side.&lt;/p&gt;

&lt;p&gt;From those it infers the whole skeleton, which is a nice reminder of how much structure a human body has. If you know where the joints are, the rest is mostly proportions.&lt;/p&gt;

&lt;p&gt;Placement matters more than you would think.&lt;/p&gt;

&lt;p&gt;My first pass had the wrists sitting on the forearms, which would have made his hands bend in the wrong place every time he shot a web.&lt;/p&gt;

&lt;p&gt;I picked &lt;strong&gt;Standard Skeleton (65)&lt;/strong&gt;, which includes finger bones, because a two handed pull without fingers looks like a man wearing oven mitts.&lt;/p&gt;

&lt;p&gt;And then it just... worked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79qoafe0w7eqtl3k2siz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79qoafe0w7eqtl3k2siz.gif" alt="Miles rigged by Mixamo, playing an animation" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rigged character goes straight into Mixamo's library of motion captured animations.&lt;/p&gt;

&lt;p&gt;These are real performers in mocap suits, recorded and cleaned up, so they have the weight shifts and tiny wobbles that hand keyed animation struggles to fake.&lt;/p&gt;

&lt;p&gt;Jumps, climbs, hangs, throws, dances, falls, the lot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4366zokae1j60wagh0mg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4366zokae1j60wagh0mg.gif" alt="Miles doing a jump to hang animation" width="760" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4ipcv8y4wekou85akbx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4ipcv8y4wekou85akbx.gif" alt="Miles doing an attack animation" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was one trade off, and it hurt a little.&lt;/p&gt;

&lt;p&gt;Mixamo builds its &lt;em&gt;own&lt;/em&gt; body skeleton.&lt;/p&gt;

&lt;p&gt;It does not know or care about the beautiful 143 bone face rig the model came with.&lt;/p&gt;
&lt;h2&gt;
  
  
  "With skin" and "without skin"
&lt;/h2&gt;

&lt;p&gt;When you download an animation, Mixamo asks whether you want it &lt;strong&gt;with skin&lt;/strong&gt; or &lt;strong&gt;without skin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a good little lesson in separating data from presentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With skin&lt;/strong&gt; gives you the mesh, the skeleton and the motion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without skin&lt;/strong&gt; gives you only the skeleton and the motion, a few hundred kilobytes of bone rotations over time.&lt;/p&gt;

&lt;p&gt;Because every clip targets the same 65 bone skeleton with the same bone names, you only need the skin &lt;em&gt;once&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Everything else is just keyframe data keyed by bone name, and Blender happily applies it to any armature whose bones match.&lt;/p&gt;

&lt;p&gt;So I downloaded the first clip with skin and the rest without.&lt;/p&gt;

&lt;p&gt;And I downloaded a lot of 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%2Fxi28jpi0m04wfin2s2ou.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%2Fxi28jpi0m04wfin2s2ou.png" alt="A Downloads folder full of Mixamo FBX files" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seventy eight files.&lt;/p&gt;

&lt;p&gt;Clicked one at a time, because Mixamo has no multi select.&lt;/p&gt;

&lt;p&gt;Some of them even share a name with a different animation inside, because the site just appends &lt;code&gt;(1)&lt;/code&gt; to whatever you downloaded last.&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%2Fosrafu77gya2e3ehwbya.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%2Fosrafu77gya2e3ehwbya.png" alt="They're the same picture meme: Hanging Idle.fbx vs Hanging Idle (1).fbx, Mixamo says they're the same picture" width="360" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They are not the same picture.&lt;/p&gt;

&lt;p&gt;The agent ended up scanning every FBX to see what was actually inside before trusting a single filename.&lt;/p&gt;
&lt;h2&gt;
  
  
  From a pile of clips to a performance
&lt;/h2&gt;

&lt;p&gt;Here is the whole pipeline, end to end:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Sketchfab model] --&amp;gt; B[Clean up in Blender&amp;lt;br/&amp;gt;relink textures, drop pink slots]
    B --&amp;gt; C[Export T-pose FBX]
    C --&amp;gt; D[Mixamo auto-rig&amp;lt;br/&amp;gt;5 markers]
    D --&amp;gt; E[Download clips&amp;lt;br/&amp;gt;1 with skin, rest without]
    E --&amp;gt; F[Import as Blender actions]
    F --&amp;gt; G{Clip looks right&amp;lt;br/&amp;gt;from this angle?}
    G --&amp;gt;|no| E
    G --&amp;gt;|yes| H[Chain clips on NLA&amp;lt;br/&amp;gt;crossfades + root motion]
    H --&amp;gt; I[Webs, board, camera]
    I --&amp;gt; J[360p preview render]
    J --&amp;gt; K{Good enough?}
    K --&amp;gt;|no| H
    K --&amp;gt;|yes| L[Final render]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef source fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef rig fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef scene fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef out fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a

    class G,K decision
    class A,B,C source
    class D,E,F rig
    class H,I scene
    class J,L out&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Each Mixamo clip lands in Blender as an &lt;strong&gt;action&lt;/strong&gt;, a named bundle of animation curves (an f-curve per bone per rotation channel).&lt;/p&gt;

&lt;p&gt;On its own, an action is a single move.&lt;/p&gt;

&lt;p&gt;A performance is several of them in a row, and that is what Blender's &lt;a href="https://docs.blender.org/manual/en/latest/editors/nla/introduction.html" rel="noopener noreferrer"&gt;Nonlinear Animation editor&lt;/a&gt; is for.&lt;/p&gt;

&lt;p&gt;The NLA is basically a video editor timeline for animation.&lt;/p&gt;

&lt;p&gt;Each action becomes a &lt;strong&gt;strip&lt;/strong&gt;, strips sit on tracks, and overlapping strips can &lt;strong&gt;blend&lt;/strong&gt; into each other.&lt;/p&gt;

&lt;p&gt;That blend is what stops a cut from "hanging" to "landing" from snapping like a broken GIF.&lt;/p&gt;

&lt;p&gt;The agent wrapped all of this into a tiny sequencer, so the shot is described as a list of moves instead of a pile of frame numbers:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;perf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Performer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;perf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hanging Idle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;perf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hard Landing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;perf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Standing 1H Magic Attack 02&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;perf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pull Heavy Object Stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;face&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;perf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Victory Idle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;face&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;perf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# NLA strips, crossfades, stitched root motion
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;blend=6&lt;/code&gt; is a six frame crossfade into the next clip.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;face=65&lt;/code&gt; turns him 65 degrees so the yank points at the board.&lt;/p&gt;

&lt;p&gt;And the "web shot" is, I promise, a clip literally named &lt;em&gt;Magic Attack&lt;/em&gt;. Mixamo does not have a "thwip".&lt;/p&gt;

&lt;p&gt;The interesting problem hiding in &lt;code&gt;build()&lt;/code&gt; is &lt;strong&gt;root motion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Some clips move the character through space (a landing drops him, a pull leans him back), and each clip starts from its own origin.&lt;/p&gt;

&lt;p&gt;Play two of them back to back naively and Miles teleports back to the start between moves.&lt;/p&gt;

&lt;p&gt;So the sequencer reads where the hips ended in one clip and offsets the next one to begin there.&lt;/p&gt;

&lt;p&gt;It is the same idea as stitching GPS tracks together: each segment is relative, so you keep a running sum.&lt;/p&gt;

&lt;p&gt;Because all timing derives from that list, swapping one clip re-times the webs, the board and the camera automatically.&lt;/p&gt;

&lt;p&gt;The web strands, the board and the camera are not Mixamo at all, they are plain Blender objects keyframed by script to fire on the frame where his hand reaches out.&lt;/p&gt;
&lt;h2&gt;
  
  
  Iterate at 360p, like a sane person
&lt;/h2&gt;

&lt;p&gt;The first full render was 16 seconds at 720p and took about eight minutes on my GTX 1650.&lt;/p&gt;

&lt;p&gt;That is fine once.&lt;/p&gt;

&lt;p&gt;It is not fine when the feedback is "make it snappier", "make the board bigger" and "his suit is too dark" in the same breath.&lt;/p&gt;

&lt;p&gt;So previews dropped to 360p, roughly 4x faster, and 720p became a finals only thing.&lt;/p&gt;

&lt;p&gt;The same rule you know from code applies: make the feedback loop fast first, then make the output good.&lt;/p&gt;

&lt;p&gt;Two more things that cost real time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The suit rendered as a flat black blob.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model used a toon shader that collapses lighting into a couple of hard bands, which looks great in a cartoon and terrible when you want to see muscles.&lt;/p&gt;

&lt;p&gt;Swapping in physically based shading, plus a light just for him and red and blue rim lights, brought his shape back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Light grey or dark studio?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A near black suit on a near black background is a silhouette of nothing.&lt;/p&gt;

&lt;p&gt;Lighter backgrounds read better. Dark backgrounds look cooler. The GIF at the top is the dark one, because I have priorities.&lt;/p&gt;
&lt;h2&gt;
  
  
  Escaping the download button
&lt;/h2&gt;

&lt;p&gt;After clicking "Download" 78 times, I asked the obvious engineer question: can I just do this locally?&lt;/p&gt;

&lt;p&gt;Mixamo's rigger and library only live on Adobe's servers.&lt;/p&gt;

&lt;p&gt;But motion capture data is not an Adobe invention.&lt;/p&gt;

&lt;p&gt;The &lt;a href="http://mocap.cs.cmu.edu/" rel="noopener noreferrer"&gt;CMU Graphics Lab Motion Capture Database&lt;/a&gt; has about 2,500 free clips, and there is a &lt;a href="https://github.com/una-dinosauria/cmu-mocap" rel="noopener noreferrer"&gt;BVH mirror on GitHub&lt;/a&gt; with a text index of what each one is.&lt;/p&gt;

&lt;p&gt;Cartwheels, salsa, climbing, martial arts, "pulling a rope", all of it.&lt;/p&gt;

&lt;p&gt;The catch is that CMU's skeleton is not Mixamo's skeleton.&lt;/p&gt;

&lt;p&gt;Moving motion from one skeleton to another is called &lt;strong&gt;retargeting&lt;/strong&gt;, and it is a lovely little linear algebra problem.&lt;/p&gt;

&lt;p&gt;You cannot just copy rotations across, because the two skeletons have different rest poses. A 30 degree rotation from "arms slightly down" is not the same pose as 30 degrees from "arms straight out".&lt;/p&gt;

&lt;p&gt;So the retargeter converts each bone's rotation out of the source rest pose, into a shared world frame, and back into the target's rest pose.&lt;/p&gt;

&lt;p&gt;It also scales the hip travel by leg length, so a tall performer's stride does not make Miles moonwalk.&lt;/p&gt;

&lt;p&gt;The first try rendered... nothing.&lt;/p&gt;

&lt;p&gt;Miles was not in frame. He was not anywhere.&lt;/p&gt;

&lt;p&gt;The BVH rest pose put the hips exactly at the origin, so "rest hip height" was &lt;strong&gt;zero&lt;/strong&gt;, and the scale factor was a division by it.&lt;/p&gt;

&lt;p&gt;Miles had been launched into orbit by a divide by zero.&lt;/p&gt;

&lt;p&gt;Fixed by measuring the floor from the lowest foot in the clip instead, and suddenly he was walking, dancing and cartwheeling from a dataset recorded at Carnegie Mellon years ago.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;I set out to make a guy swing on a web.&lt;/p&gt;

&lt;p&gt;I came back with a list of concepts I did not expect from an art tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A character is a &lt;strong&gt;mesh&lt;/strong&gt; deformed by a &lt;strong&gt;tree of bones&lt;/strong&gt;, via per vertex &lt;strong&gt;weights&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Animation is &lt;strong&gt;rotations relative to a rest pose&lt;/strong&gt;, which is why everything T-poses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IK&lt;/strong&gt; is a constraint solver, and it is the difference between a puppet and a corpse.&lt;/li&gt;
&lt;li&gt;Motion clips are &lt;strong&gt;data keyed by bone name&lt;/strong&gt;, so one skin can wear a thousand animations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NLA strips&lt;/strong&gt; are a timeline of reusable actions, and chaining them means stitching &lt;strong&gt;root motion&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retargeting&lt;/strong&gt; is a change of basis, and it will divide by zero if you let it.&lt;/li&gt;
&lt;li&gt;Blender is a Python program with a UI on top, which is why an &lt;strong&gt;MCP bridge&lt;/strong&gt; can drive it at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a ton more in there for a CS person: shader node graphs are dataflow programs, Cycles is a path tracer, physics sims are numerical integrators, geometry nodes are basically a functional language for meshes.&lt;/p&gt;

&lt;p&gt;But one step at a time.&lt;/p&gt;

&lt;p&gt;Next on the list is the face rig I so cruelly abandoned in that pool, and maybe making Miles actually talk.&lt;/p&gt;

&lt;p&gt;If you have played with Blender, rigging or the MCP, tell me what I should pick up next.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Blast-Radius Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is an AI code reviewer that scores every hunk of a diff by &lt;strong&gt;blast radius&lt;/strong&gt;: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;blast-radius-demo.mp4&lt;/span&gt;
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview's Blast Radius &amp;amp; Review Priority scoring, live in the diff viewer.&lt;/i&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;The exact math, not a black box&lt;/th&gt;

&lt;th&gt;Visualize blast radius at a glance&lt;/th&gt;

&lt;th&gt;Every factor that feeds the score&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-3.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-3.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-4.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-4.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-2.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-2.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

How does Blast Radius scoring work? (a more technical explanation)

&lt;p&gt;&lt;strong&gt;Here's the goal:&lt;/strong&gt;&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.&lt;/li&gt;

&lt;li&gt;A 300-line UI change in one file, fully covered by tests…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blender</category>
      <category>mcp</category>
      <category>ai</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What Happens When Two Databases Both Say Yes</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Thu, 01 Oct 2026 19:22:55 +0000</pubDate>
      <link>https://dev.to/lovestaco/what-happens-when-two-databases-both-say-yes-43n1</link>
      <guid>https://dev.to/lovestaco/what-happens-when-two-databases-both-say-yes-43n1</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your database has one primary and two replicas, and for about two years that is the best decision you ever made.&lt;/p&gt;

&lt;p&gt;Reads scale out across the replicas. Writes all go to the one primary. &lt;/p&gt;

&lt;p&gt;There is exactly one copy of the truth, and it lives on a machine you could point at.&lt;/p&gt;

&lt;p&gt;Then sales closes a deal in Sydney.&lt;/p&gt;

&lt;h2&gt;
  
  
  Physics sends you an invoice
&lt;/h2&gt;

&lt;p&gt;Your primary is in Virginia. Your new users are in Sydney.&lt;/p&gt;

&lt;p&gt;Every write from Sydney now has to cross the Pacific, get acknowledged, and come back. &lt;/p&gt;

&lt;p&gt;That is roughly a 200 millisecond round trip, and no amount of clever indexing touches it, because the delay is not your database being slow.&lt;/p&gt;

&lt;p&gt;It is light being inconveniently finite.&lt;/p&gt;

&lt;p&gt;Two hundred milliseconds does not sound fatal until you notice how many writes hide inside one user action.&lt;/p&gt;

&lt;p&gt;A session token. An analytics event. A "last seen" bump. Suddenly a page that feels instant in Boston feels like dialup in Sydney.&lt;/p&gt;

&lt;p&gt;And then there is the other problem, the one that shows up on a Saturday.&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%2Fcnvusbf49j8qvvx1unk3.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%2Fcnvusbf49j8qvvx1unk3.png" alt="Diagram: a single primary in Virginia with two read replicas, a Boston user writing in 12ms, a Sydney user paying a 200ms round trip for every write, and the primary crossed out in red with a box reading " width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If us-east-1 has a bad day, nobody writes anything. Not Sydney, not Boston, not the replicas sitting right there with a full copy of the data.&lt;/p&gt;

&lt;p&gt;They have the bytes. They are just not allowed to touch them.&lt;/p&gt;

&lt;p&gt;One leader means one place to be slow and one place to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  So you give each region its own leader
&lt;/h2&gt;

&lt;p&gt;This is multi-leader replication, and the idea is exactly as blunt as it sounds.&lt;/p&gt;

&lt;p&gt;Put a leader in each region. Let each one accept writes from the users nearest to it. Then have the leaders ship their changes to each other in the background.&lt;/p&gt;

&lt;p&gt;Sydney writes land in Sydney at 8 milliseconds. Virginia writes land in Virginia at 8 milliseconds. &lt;/p&gt;

&lt;p&gt;A few seconds later, each change has made its way to the other side of the world and both leaders hold the same data.&lt;/p&gt;

&lt;p&gt;You just bought two genuinely good things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write latency collapses&lt;/strong&gt; for everyone who is not sitting next to your original region. Writes are now local.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Losing a region is survivable.&lt;/strong&gt; Sydney goes dark, Virginia keeps taking writes, and when Sydney comes back it catches up from the log.&lt;/p&gt;

&lt;p&gt;Your write availability is no longer the uptime of a single building.&lt;/p&gt;

&lt;p&gt;This is the same shape as single-leader replication, just with the "single" removed. Which is a sentence that should make you slightly nervous, and it should.&lt;/p&gt;

&lt;p&gt;Because the thing that made single-leader so pleasant to reason about was never the leader. It was the &lt;em&gt;one&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill arrives as a row number
&lt;/h2&gt;

&lt;p&gt;Alan is in Sydney. He opens row 1,000,000 and sets &lt;code&gt;x = 7&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At more or less the same moment, John is in Virginia. He opens the same row and sets &lt;code&gt;x = 9&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sydney accepts Alan's write and returns a 200. Virginia accepts John's write and returns a 200. &lt;/p&gt;

&lt;p&gt;Neither leader hesitated, because at the moment each of them decided, the other write did not exist yet as far as they could tell.&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%2F8ycdyndplosrm4nn52vf.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%2F8ycdyndplosrm4nn52vf.png" alt="Diagram: Alan writing x=7 to the Sydney leader and John writing x=9 to the Virginia leader at the same moment, both committing and returning 200 OK, with replication still in flight between them, over a red box asking " width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the replication streams cross in the middle of the ocean, and both leaders simultaneously learn that they were lied to.&lt;/p&gt;

&lt;p&gt;So what is &lt;code&gt;x&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;There is no answer to that question hiding in the data. The database has already promised both values to two different humans. &lt;/p&gt;

&lt;p&gt;Somebody is about to be wrong and nobody has told them yet.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;the&lt;/strong&gt; problem with multi-leader replication. Not a problem. The problem. Everything else in this post is a response to it.&lt;/p&gt;

&lt;p&gt;And notice what single-leader gave you for free here. &lt;/p&gt;

&lt;p&gt;With one primary, these two writes would have arrived at the same machine, one would have gone second, and it would have overwritten the first &lt;em&gt;while knowing it was overwriting something&lt;/em&gt;. A &lt;code&gt;SELECT&lt;/code&gt; in between would have seen a consistent story.&lt;/p&gt;

&lt;p&gt;That is not a feature anybody shipped. It is a side effect of there being one queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  "At the same time" is the wrong mental model
&lt;/h2&gt;

&lt;p&gt;It is worth being precise here, because this is where people's intuition quietly breaks.&lt;/p&gt;

&lt;p&gt;Alan and John did not have to write at the same instant for this to happen. They could have been forty seconds apart.&lt;/p&gt;

&lt;p&gt;Two writes are &lt;strong&gt;concurrent&lt;/strong&gt; if neither one knew about the other. That is the entire definition. &lt;/p&gt;

&lt;p&gt;It has nothing to do with wall clock time and everything to do with what information had reached whom.&lt;/p&gt;

&lt;p&gt;Leslie Lamport worked this out in 1978 in &lt;a href="https://lamport.azurewebsites.net/pubs/time-clocks.pdf" rel="noopener noreferrer"&gt;Time, Clocks, and the Ordering of Events in a Distributed System&lt;/a&gt;, and it is still the single most useful thing you can read on the subject.&lt;/p&gt;

&lt;p&gt;If Alan's write had reached Virginia before John started typing, John's write would be a normal overwrite. Not a conflict, just an edit.&lt;/p&gt;

&lt;p&gt;Replication lag is what turns an edit into a conflict.&lt;/p&gt;

&lt;p&gt;Which means your conflict rate is not a property of your users. It is a property of how long your replication takes, multiplied by how much your users overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default answer is "last write wins", and it is worse than it sounds
&lt;/h2&gt;

&lt;p&gt;Most systems that offer multi-leader replication ship with a default conflict resolution policy, and that default is almost always &lt;strong&gt;last write wins&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;DynamoDB &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GlobalTables.html" rel="noopener noreferrer"&gt;global tables&lt;/a&gt; do it. Cassandra does it. It is the policy you get when nobody chose a policy.&lt;/p&gt;

&lt;p&gt;Each write carries a timestamp. When two versions of a row collide, the one with the bigger timestamp survives. The other one is discarded.&lt;/p&gt;

&lt;p&gt;Not flagged. Not queued for review. Discarded.&lt;/p&gt;

&lt;p&gt;Here is the part that gets people. &lt;strong&gt;LWW is a guaranteed data loss mechanism&lt;/strong&gt;, and that is not a criticism, that is its job description. &lt;/p&gt;

&lt;p&gt;If two writes conflict and you keep exactly one, you have deliberately thrown away a write that the database told a user it had accepted.&lt;/p&gt;

&lt;p&gt;Sometimes that is completely fine. If the field is "last known GPS position", dropping the older one is correct and you should feel nothing.&lt;/p&gt;

&lt;p&gt;If the field is "account balance", you have just invented a very exciting kind of bug.&lt;/p&gt;

&lt;p&gt;And then there is the detail that turns a tolerable policy into a haunted one.&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%2Fa4w9ipovxana3vy64cil.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%2Fa4w9ipovxana3vy64cil.png" alt="Diagram: a real-time axis showing Alan writing x=7 first and John writing x=9 eighty milliseconds later, then the stored timestamps where Sydney's 120ms-fast clock stamps Alan's write at .124 and Virginia stamps John's at .084, so LWW picks x=7 and silently discards the genuinely newer write" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whose clock?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LWW compares timestamps generated by two different machines, and those machines do not agree on what time it is. &lt;/p&gt;

&lt;p&gt;NTP keeps them roughly in sync, where "roughly" means tens of milliseconds on a good day and considerably worse on a VM whose host is under load or whose clock just stepped.&lt;/p&gt;

&lt;p&gt;So if Sydney's clock has drifted 120 milliseconds ahead, Alan's &lt;em&gt;earlier&lt;/em&gt; write gets stamped with a &lt;em&gt;later&lt;/em&gt; timestamp.&lt;/p&gt;

&lt;p&gt;LWW dutifully keeps it and throws away John's write, which actually happened second.&lt;/p&gt;

&lt;p&gt;The system did exactly what it was configured to do. It also reverted a value, told the user it had succeeded, and left no trace anywhere that it happened.&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%2Fffmj1v671cqyzbzcz7wd.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%2Fffmj1v671cqyzbzcz7wd.png" alt="What Year Is It meme captioned " width="360" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the reason Google built &lt;a href="https://cloud.google.com/spanner/docs/true-time-external-consistency" rel="noopener noreferrer"&gt;TrueTime&lt;/a&gt; for Spanner, with GPS receivers and atomic clocks in every datacenter, and then had Spanner deliberately &lt;em&gt;wait out&lt;/em&gt; the clock uncertainty interval before committing. &lt;/p&gt;

&lt;p&gt;They did not solve clock skew. They bounded it, paid for the bound in hardware, and then paid for it again in commit latency.&lt;/p&gt;

&lt;p&gt;If you are doing LWW on timestamps from &lt;code&gt;now()&lt;/code&gt; across regions, you are making the same correctness claim Spanner makes, with none of the equipment.&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%2Fmtd3dwvb81f1cwxprshg.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%2Fmtd3dwvb81f1cwxprshg.png" alt="Guy Hammering Nails Into Sand meme, the hammer labelled " width="360" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do instead of picking a loser
&lt;/h2&gt;

&lt;p&gt;The good news is that LWW is a default, not a law. &lt;/p&gt;

&lt;p&gt;Once you accept that conflicts are going to happen, you get to choose what happens next, and there are better options than a coin flip weighted by NTP drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep both and let the application decide.&lt;/strong&gt; CouchDB does this properly: a conflicting document keeps every version as a &lt;a href="https://docs.couchdb.org/en/stable/replication/conflicts.html" rel="noopener noreferrer"&gt;sibling&lt;/a&gt;, picks one deterministically so reads do not break, and leaves the rest sitting there for you to resolve. &lt;/p&gt;

&lt;p&gt;Riak called these siblings too. It is the most honest option, and it is the one that forces you to actually write resolution logic, which is why people avoid it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write a merge function.&lt;/strong&gt; For a lot of real data, "pick a winner" is just the wrong operation. Two users adding different items to a shared cart is a union, not a race. &lt;/p&gt;

&lt;p&gt;Two users editing different fields of the same profile row is two independent edits that a column-level merge handles without anyone losing anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change the data type so conflicts cannot exist.&lt;/strong&gt; This is the good one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replicate the operation, not the value
&lt;/h2&gt;

&lt;p&gt;Here is the move that makes a whole class of conflicts evaporate.&lt;/p&gt;

&lt;p&gt;A conflict happens because two leaders made competing claims about &lt;em&gt;what a value is&lt;/em&gt;. Sydney said the cart has 4 items.&lt;/p&gt;

&lt;p&gt;Virginia said the cart has 4 items. They both read 3 and both added 1.&lt;/p&gt;

&lt;p&gt;Last write wins picks one of those two 4s, and the answer is 4. The customer added two things and the cart grew by one.&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%2Flazsz4d1999a9pqkcgbc.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%2Flazsz4d1999a9pqkcgbc.png" alt="Diagram: on the left, both leaders reading 3 and setting 4, converging on cart = 4 with one item silently lost; on the right, Sydney recording syd += 1 and Virginia recording vir += 1, summing to cart = 5" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now watch what happens if the leaders stop shipping the &lt;em&gt;result&lt;/em&gt; and start shipping the &lt;em&gt;operation&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Sydney does not say "the cart is 4". It says "Sydney has contributed 1". Virginia says "Virginia has contributed 1". &lt;/p&gt;

&lt;p&gt;The value of the cart is the sum of everyone's contributions, and both leaders compute 5 without ever disagreeing about anything.&lt;/p&gt;

&lt;p&gt;Nothing had to win, because the two writes were never competing claims in the first place.&lt;/p&gt;

&lt;p&gt;That is a &lt;a href="https://crdt.tech/" rel="noopener noreferrer"&gt;CRDT&lt;/a&gt;, and the counter version of it is so simple you can build one in plain SQL with no special database at all:&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="c1"&gt;-- one row per (cart, replica). each leader only ever touches its own row,&lt;/span&gt;
&lt;span class="c1"&gt;-- so two leaders can never write the same row and never conflict.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cart_counter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;cart_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;replica&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 'syd' | 'iad' | 'fra'&lt;/span&gt;
  &lt;span class="n"&gt;delta&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&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;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Sydney's leader, adding an item. it writes replica = 'syd', always.&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;cart_counter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1841&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'syd'&lt;/span&gt;&lt;span class="p"&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;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cart_counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delta&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="c1"&gt;-- the actual value, which every region computes identically&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cart_counter&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;cart_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1841&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The write set of each leader is disjoint by construction. Replication can arrive in any order, duplicated, delayed by a week, and every region still converges on the same number.&lt;/p&gt;

&lt;p&gt;This is the same principle behind &lt;a href="https://automerge.org/" rel="noopener noreferrer"&gt;Automerge&lt;/a&gt; and &lt;a href="https://yjs.dev/" rel="noopener noreferrer"&gt;Yjs&lt;/a&gt;, which is how collaborative editors let twelve people type into one paragraph without a central referee. &lt;/p&gt;

&lt;p&gt;And it is worth noticing that a Google Doc or a Figma file &lt;em&gt;is&lt;/em&gt; a multi-leader system. Every laptop is a leader. &lt;/p&gt;

&lt;p&gt;The offline mode you shipped last quarter made your mobile app one too.&lt;/p&gt;

&lt;p&gt;The catch is that CRDTs are not free. They carry per-replica metadata, which grows, and they are only convenient for data types somebody has already worked out the merge semantics for: counters, sets, registers, sequences, maps. &lt;/p&gt;

&lt;p&gt;The moment your business rule is "the total must never exceed the credit limit", you are back to needing coordination, because that is a global invariant and CRDTs specifically do not give you those.&lt;/p&gt;
&lt;h2&gt;
  
  
  The conflicts nobody warns you about
&lt;/h2&gt;

&lt;p&gt;So far this has all been about one row, one column. The conflicts that actually ruin a migration weekend are the structural ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-increment primary keys.&lt;/strong&gt; Two leaders, both confidently handing out id 4817 to different rows. The fix is for each leader to generate ids from a disjoint space: offset sequences, UUIDs, or a &lt;a href="https://en.wikipedia.org/wiki/Snowflake_ID" rel="noopener noreferrer"&gt;Snowflake-style id&lt;/a&gt; with the region baked into the bits. If you are turning on a second leader and have not solved this, solve it first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unique constraints.&lt;/strong&gt; Two users in two regions both register &lt;code&gt;alan@example.com&lt;/code&gt;. Each leader checks its local table, sees nothing, and allows it. &lt;/p&gt;

&lt;p&gt;The constraint held locally and is violated globally, which is a fun error to discover during replication rather than during signup. Uniqueness is a global invariant and asynchronous replication fundamentally cannot enforce one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foreign keys and deletes.&lt;/strong&gt; Sydney deletes a parent row. Virginia inserts a child pointing at it. Both are valid locally. &lt;/p&gt;

&lt;p&gt;Together they produce an orphan, and now your replication stream is stuck trying to apply something your schema refuses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Triggers and defaults that call &lt;code&gt;now()&lt;/code&gt;.&lt;/strong&gt; Any logic that is not a pure function of the row will produce different results on different leaders. Replicate the computed value, not the computation.&lt;/p&gt;

&lt;p&gt;Notice the pattern. Everything on that list is a constraint that spans rows your leaders do not coordinate on. &lt;/p&gt;

&lt;p&gt;Single-leader hid all of them behind one serialisation point, for free, and you never knew they were being handled.&lt;/p&gt;
&lt;h2&gt;
  
  
  The strategy that actually ships
&lt;/h2&gt;

&lt;p&gt;Here is what teams running this in production actually do, and it is less glamorous than any of the above.&lt;/p&gt;

&lt;p&gt;They make the conflict impossible.&lt;/p&gt;

&lt;p&gt;Give every record exactly one &lt;strong&gt;home leader&lt;/strong&gt; that owns its writes. All writes for a given customer, tenant, or account are routed to that customer's home region, no matter where the request physically landed.&lt;/p&gt;

&lt;p&gt;Every leader still replicates everything to everywhere, so reads stay local and failover stays possible.&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%2F10rmvl3tfjh33173664r.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%2F10rmvl3tfjh33173664r.png" alt="Diagram: a router reading home_region from the account row and sending APAC account writes to the Sydney leader and US account writes to the Virginia leader, each owning a disjoint slice of accounts, with async replication flowing both ways, and a yellow box noting " width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the two leaders have &lt;strong&gt;disjoint write sets&lt;/strong&gt;. Sydney writes rows Virginia never writes. There is no conflict to resolve because the two leaders are never making claims about the same data.&lt;/p&gt;

&lt;p&gt;Conflict resolution logic you never run is the only kind that never has a bug.&lt;/p&gt;

&lt;p&gt;This is why multi-leader works beautifully when your data has a natural ownership boundary. Per-customer SaaS. Per-tenant workspaces. Per-region marketplaces. &lt;/p&gt;

&lt;p&gt;It works much less beautifully for a global leaderboard or a shared inventory count, where every write touches data everybody else touches too.&lt;/p&gt;

&lt;p&gt;The routing itself is usually unexciting, which is the point:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_region&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home_region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# cached, changes rarely
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;home&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;request_region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;local_leader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                 &lt;span class="c1"&gt;# the fast path, ~8ms
&lt;/span&gt;
    &lt;span class="c1"&gt;# cross-region write: slower, but still exactly one leader owns this row,
&lt;/span&gt;    &lt;span class="c1"&gt;# so it is an ordinary overwrite rather than a conflict waiting to converge
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;leader_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note what that &lt;code&gt;else&lt;/code&gt; branch is buying you. A European user acting on a US-homed account pays the 100 millisecond hop.&lt;/p&gt;

&lt;p&gt;You have traded &lt;em&gt;some&lt;/em&gt; users' write latency for never having to explain to your CFO why a balance went backwards.&lt;/p&gt;

&lt;p&gt;That is usually the right trade, and it is a trade, not a free lunch.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Write arrives in Frankfurt] --&amp;gt; B{Which region&amp;lt;br/&amp;gt;owns this account?}
    B --&amp;gt;|Frankfurt| C[Local leader, 8 ms]
    B --&amp;gt;|Virginia| D[Forward to Virginia, 100 ms]
    C --&amp;gt; E[Commit, ack the user]
    D --&amp;gt; E
    E --&amp;gt; F[Async replication to every other leader]
    F --&amp;gt; G[All regions can serve reads&amp;lt;br/&amp;gt;and take over on failover]
    B --&amp;gt;|Home region is down| H{Can you wait&amp;lt;br/&amp;gt;for it to come back?}
    H --&amp;gt;|Yes| I[Reject the write, stay conflict-free]
    H --&amp;gt;|No| J[Reassign the home, accept&amp;lt;br/&amp;gt;that you may now conflict]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef good fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef warn fill:#ff9a5c,stroke:#c25f20,color:#1a1a1a

    class A start
    class B,H decision
    class C,D,E,F,G good
    class I,J warn&lt;/code&gt;&lt;/pre&gt;


&lt;h2&gt;
  
  
  The honest caveat: homes move
&lt;/h2&gt;

&lt;p&gt;Ownership routing is clean right up until the moment a region fails, which is the exact scenario you built multi-leader for in the first place.&lt;/p&gt;

&lt;p&gt;Sydney goes down. Its accounts have no home. &lt;/p&gt;

&lt;p&gt;You either reject their writes, which defeats the point, or you reassign their home to Virginia, which means accepting writes for rows Sydney might also have accepted writes for in the seconds before it died but not yet replicated.&lt;/p&gt;

&lt;p&gt;That window is where your conflicts live. Not during normal operation, during failover.&lt;/p&gt;

&lt;p&gt;So the real design is: disjoint write sets almost always, plus a conflict resolution policy you have genuinely thought about for the minutes per year when they are not disjoint. &lt;/p&gt;

&lt;p&gt;Teams that skip the second half discover they needed it during an incident, which is the worst possible time to be reading about version vectors.&lt;/p&gt;

&lt;p&gt;Two smaller things that bite, worth knowing before they bite you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading your own writes gets harder.&lt;/strong&gt; A user writes to their home leader in Virginia, then their next request hits a read replica in Frankfurt that is 400 milliseconds behind. &lt;/p&gt;

&lt;p&gt;Their change is gone. You fix this by pinning a user's session to one region for a while after a write, or by having the client carry the version it last saw and the read wait for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replication topology matters.&lt;/strong&gt; All-to-all is the usual choice and is the most robust, but it lets messages overtake each other, so an update can arrive before the insert it depends on. &lt;/p&gt;

&lt;p&gt;Circular and star topologies are cheaper on connections and much worse on failure, because one dead node breaks the chain. MySQL's circular replication is famous for exactly this.&lt;/p&gt;
&lt;h2&gt;
  
  
  So should you turn this on?
&lt;/h2&gt;

&lt;p&gt;The uncomfortable truth is that most teams reaching for multi-leader replication do not need it.&lt;/p&gt;

&lt;p&gt;If your problem is read latency, you want read replicas, and you already have them. Multi-leader only pays off when &lt;strong&gt;writes&lt;/strong&gt; are the thing crossing an ocean.&lt;/p&gt;

&lt;p&gt;If your problem is regional failover, a single leader with a well-rehearsed promotion runbook gets you most of the way there, with a fraction of the semantics to reason about.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Users in multiple regions] --&amp;gt; B{Are WRITES the&amp;lt;br/&amp;gt;slow part?}
    B --&amp;gt;|No, reads are| C[Read replicas per region&amp;lt;br/&amp;gt;Keep the single leader]
    B --&amp;gt;|Yes| D{Does the data split&amp;lt;br/&amp;gt;cleanly by owner?}
    D --&amp;gt;|Yes, per tenant or account| E[Multi-leader with home regions&amp;lt;br/&amp;gt;Disjoint write sets]
    D --&amp;gt;|No, everyone writes everything| F{Can you model it&amp;lt;br/&amp;gt;as a CRDT?}
    F --&amp;gt;|Yes, counters sets text| G[CRDTs, converge without picking]
    F --&amp;gt;|No, you need real invariants| H[Distributed SQL&amp;lt;br/&amp;gt;Spanner, CockroachDB, YugabyteDB]
    E --&amp;gt; I[Still need a policy for&amp;lt;br/&amp;gt;the failover window]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef good fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef alt fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef warn fill:#ff9a5c,stroke:#c25f20,color:#1a1a1a

    class A start
    class B,D,F decision
    class C,E good
    class G,H alt
    class I warn&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;And if you genuinely need strong consistency across regions, that is what &lt;a href="https://www.cockroachlabs.com/docs/stable/architecture/replication-layer" rel="noopener noreferrer"&gt;CockroachDB&lt;/a&gt; and Spanner are for.&lt;/p&gt;

&lt;p&gt;They run consensus under the hood and charge you for it in write latency, openly, instead of handing you an asynchronous system that looks consistent until it is not.&lt;/p&gt;

&lt;p&gt;That is the actual choice. Not "multi-leader or single-leader", but &lt;em&gt;where you would like to pay&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Single-leader charges distant users on every write. Distributed SQL charges everyone a smaller amount on every write. &lt;/p&gt;

&lt;p&gt;Multi-leader charges nobody anything on the happy path and sends you one enormous invoice on the day two regions disagree about a row.&lt;/p&gt;

&lt;p&gt;Pick the bill you would rather receive at 3am.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Multi-leader replication does not create conflicts. &lt;/p&gt;

&lt;p&gt;It reveals that your data model always had them, and that a single primary was quietly doing your reconciliation for free.&lt;/p&gt;

&lt;p&gt;The moment you add a second leader, that work moves to you. &lt;/p&gt;

&lt;p&gt;You can do it with a timestamp comparison and hope, or with disjoint write sets and intent.&lt;/p&gt;

&lt;p&gt;Only one of those lets you sleep.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Blast-Radius Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is an AI code reviewer that scores every hunk of a diff by &lt;strong&gt;blast radius&lt;/strong&gt;: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;blast-radius-demo.mp4&lt;/span&gt;
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview's Blast Radius &amp;amp; Review Priority scoring, live in the diff viewer.&lt;/i&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;The exact math, not a black box&lt;/th&gt;

&lt;th&gt;Visualize blast radius at a glance&lt;/th&gt;

&lt;th&gt;Every factor that feeds the score&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-3.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-3.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-4.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-4.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-2.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-2.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

How does Blast Radius scoring work? (a more technical explanation)

&lt;p&gt;&lt;strong&gt;Here's the goal:&lt;/strong&gt;&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.&lt;/li&gt;

&lt;li&gt;A 300-line UI change in one file, fully covered by tests…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>database</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building a Custom ERP Module on the MERN Stack: Data Modeling, Transactions and Audit Trails</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Tue, 29 Sep 2026 21:25:00 +0000</pubDate>
      <link>https://dev.to/lovestaco/building-a-custom-erp-module-on-the-mern-stack-data-modeling-transactions-and-audit-trails-4516</link>
      <guid>https://dev.to/lovestaco/building-a-custom-erp-module-on-the-mern-stack-data-modeling-transactions-and-audit-trails-4516</guid>
      <description>&lt;p&gt;ERP software handles money, stock, and approvals, so small bugs get expensive fast. A stock count that drifts by a few units or a rounding error in an invoice can take days to trace. &lt;/p&gt;

&lt;p&gt;Off-the-shelf ERPs cover the general case well, but many companies eventually need a custom module for a workflow the big platforms handle awkwardly.&lt;/p&gt;

&lt;p&gt;The MERN stack works well for this. You get flexible schemas for evolving business rules, one language across the whole stack, and real multi-document transactions in MongoDB.&lt;/p&gt;

&lt;p&gt;This guide walks through a small purchasing and inventory module and focuses on 3 things: data modeling, consistent writes, and audit trails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Example Module
&lt;/h2&gt;

&lt;p&gt;The module has suppliers, products, purchase orders (POs) with line items, and stock levels per warehouse. &lt;/p&gt;

&lt;p&gt;The critical operation is receiving goods: the system must update PO line quantities, change the PO status, and increase stock, all at once. If only half of that happens, the numbers stop matching reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Modeling: Embed What Belongs Together
&lt;/h2&gt;

&lt;p&gt;A simple rule works well for ERP data. Embed things that live and die with the parent, like PO line items. &lt;/p&gt;

&lt;p&gt;Reference things with their own lifecycle, like suppliers and products.&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;Schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mongoose&lt;/span&gt;&lt;span class="dl"&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;lineItemSchema&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;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="c1"&gt;// snapshot at order time&lt;/span&gt;
  &lt;span class="na"&gt;unitPriceCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;min&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="na"&gt;quantityOrdered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;quantityReceived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&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="na"&gt;min&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;purchaseOrderSchema&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;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;unique&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="na"&gt;supplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Supplier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Warehouse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partially_received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&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="na"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lineItemSchema&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timestamps&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="na"&gt;optimisticConcurrency&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="nx"&gt;purchaseOrderSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;supplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stockLevelSchema&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;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Warehouse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;onHand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&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="na"&gt;min&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="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;stockLevelSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unique&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few choices here are worth copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot fixed values.&lt;/strong&gt; SKU and price are copied into each line, so old orders keep what was actually agreed even if the product changes later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store money as integers.&lt;/strong&gt; JavaScript floats can't represent 0.1 exactly. Cents avoid a whole category of rounding bugs. If you need fractional cents, use &lt;code&gt;Decimal128&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on optimistic concurrency.&lt;/strong&gt; If 2 people edit the same PO at once, the second save fails instead of silently overwriting the first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a unique compound index on stock.&lt;/strong&gt; One row per product per warehouse, guaranteed by the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also pays to design indexes around the questions people will actually ask. &lt;/p&gt;

&lt;p&gt;Procurement teams usually want "open orders from supplier X, newest first", and the compound index on &lt;code&gt;supplier&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;createdAt&lt;/code&gt; answers that without a collection scan. &lt;/p&gt;

&lt;p&gt;Look at the reports and list screens in your mockups before writing schemas, and add indexes for them from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions: Keep Related Writes Consistent
&lt;/h2&gt;

&lt;p&gt;Receiving goods touches 2 collections. Without a transaction, a crash between the 2 writes leaves you with phantom inventory. &lt;/p&gt;

&lt;p&gt;MongoDB transactions need a replica set; locally, a single-node one (&lt;code&gt;mongod --replSet rs0&lt;/code&gt;) is enough. &lt;/p&gt;

&lt;p&gt;The easiest API is &lt;code&gt;session.withTransaction()&lt;/code&gt;, which commits for you and retries on transient errors:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;receiveGoods&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;poId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receipts&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;session&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;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;po&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;PurchaseOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&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;po&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partially_received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="s1"&gt;Order cannot receive goods&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;for &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;lineId&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="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;receipts&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;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lineId&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="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantityReceived&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantityOrdered&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="s2"&gt;`Over-receipt on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&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="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantityReceived&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;quantity&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;StockLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warehouse&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;onHand&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="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;upsert&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="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantityReceived&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantityOrdered&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partially_received&lt;/span&gt;&lt;span class="dl"&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;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endSession&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;3 rules keep this safe.&lt;/p&gt;

&lt;p&gt;1) Pass the session to every read and write.&lt;br&gt;
2) Keep side effects like emails or payment calls out of the callback, since &lt;code&gt;withTransaction&lt;/code&gt; may run it more than once.&lt;br&gt;
3) Keep transactions short to avoid lock contention.&lt;/p&gt;

&lt;p&gt;For side effects that must happen, the outbox pattern works nicely. Inside the transaction, insert a record like &lt;code&gt;{ type: 'po.received', poId }&lt;/code&gt; into an &lt;code&gt;outbox&lt;/code&gt; collection. &lt;/p&gt;

&lt;p&gt;A background worker picks these up after the commit and sends the email or notifies the accounting system.&lt;/p&gt;

&lt;p&gt;If the transaction rolls back, the outbox record disappears too, so nothing gets sent for a change that never happened.&lt;/p&gt;

&lt;p&gt;Retry semantics and write conflicts rarely show up in tutorials, which is why companies building finance-adjacent modules often look for &lt;a href="https://incora.software/hire/mern-stack-developers" rel="noopener noreferrer"&gt;MERN stack developers&lt;/a&gt; who have already debugged these issues under real load. &lt;/p&gt;

&lt;p&gt;Warehouse networks are also flaky, so give each receipt an idempotency key, store it under a unique index inside the same transaction, and duplicate requests will fail harmlessly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Audit Trails: Who Changed What, and When
&lt;/h2&gt;

&lt;p&gt;Auditors will ask "who approved this order?" and "why did the price change?". A Mongoose plugin combined with &lt;code&gt;AsyncLocalStorage&lt;/code&gt; answers these automatically:&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;AsyncLocalStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:async_hooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lodash/get&lt;/span&gt;&lt;span class="dl"&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;auditContext&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;AsyncLocalStorage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Express middleware, placed after authentication&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withAuditContext&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;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;auditContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requestId&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;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;auditPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;init&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &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;$locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;depopulate&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="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;auditContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&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;changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&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="nf"&gt;directModifiedPaths&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;to&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]));&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AuditLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entityId&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;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;action&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;isNew&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;session&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="nf"&gt;$session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;purchaseOrderSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auditPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PurchaseOrder&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;The key detail is &lt;code&gt;this.$session()&lt;/code&gt;. The audit entry joins the same transaction as the change it describes, so a rollback removes both.&lt;/p&gt;

&lt;p&gt;Small details like this are easy to miss, and a review from &lt;a href="https://incora.software/hire/mern-stack-developers" rel="noopener noreferrer"&gt;experienced MERN developers&lt;/a&gt; at this stage usually costs far less than cleaning up inconsistent audit data after a compliance check.&lt;/p&gt;

&lt;p&gt;2 more things to handle. Query-level updates like the &lt;code&gt;$inc&lt;/code&gt; on stock skip save hooks, so for inventory use a &lt;code&gt;StockMovement&lt;/code&gt; ledger with one row per movement and treat &lt;code&gt;onHand&lt;/code&gt; as a cached total. &lt;/p&gt;

&lt;p&gt;The ledger also lets you rebuild stock for any date, which accountants love at month-end. And make the log append-only by giving the app's database user only &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;find&lt;/code&gt; rights on the audit collection.&lt;/p&gt;

&lt;p&gt;On the React side, a simple timeline under each record showing who changed which field and when goes a long way. &lt;/p&gt;

&lt;p&gt;Once users can see the history themselves, many "who changed this?" support tickets simply stop coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Hard Parts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mongodb-memory-server&lt;/code&gt; includes &lt;code&gt;MongoMemoryReplSet&lt;/code&gt;, so transactions work in CI without a real cluster. Focus your integration tests on the failure paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 concurrent receipts on the same PO, where one should fail on the version check.&lt;/li&gt;
&lt;li&gt;A thrown error halfway through &lt;code&gt;receiveGoods&lt;/code&gt;, after which stock, PO and audit rows should all be unchanged.&lt;/li&gt;
&lt;li&gt;The same request sent twice with one idempotency key, which should change stock only once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Embed data that belongs to its parent, store money as integers, and index for the reports people actually use. &lt;/p&gt;

&lt;p&gt;Wrap related writes in &lt;code&gt;withTransaction&lt;/code&gt;, move side effects to an outbox and write audit entries inside the same transaction. &lt;/p&gt;

&lt;p&gt;Get these right, and the rest of the module becomes ordinary CRUD work, which is exactly how ERP code should feel.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Two-Phase Commit Is a Trap: How Real Systems Do Distributed Transactions</title>
      <dc:creator>Athreya aka Maneshwar</dc:creator>
      <pubDate>Mon, 28 Sep 2026 21:55:38 +0000</pubDate>
      <link>https://dev.to/lovestaco/two-phase-commit-is-a-trap-how-real-systems-do-distributed-transactions-9i9</link>
      <guid>https://dev.to/lovestaco/two-phase-commit-is-a-trap-how-real-systems-do-distributed-transactions-9i9</guid>
      <description>&lt;p&gt;&lt;em&gt;Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. &lt;a href="https://github.com/HexmosTech/LiveReview/" rel="noopener noreferrer"&gt;Star us&lt;/a&gt; to help devs discover the project, give it a try, and share your feedback to help improve the product.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When you are first building an application, a transaction is the cheapest guarantee you will ever get.&lt;/p&gt;

&lt;p&gt;A customer places an order. You charge their card, reserve the inventory, write a ledger entry for accounting.&lt;/p&gt;

&lt;p&gt;You wrap the whole thing in &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt;, and if any one of those writes falls over, the database quietly rewinds the other two.&lt;/p&gt;

&lt;p&gt;It is as if the order never happened. You probably wrote that code in four minutes and never thought about it again.&lt;/p&gt;

&lt;p&gt;What you were leaning on there are two of the ACID guarantees, doing unpaid labour on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atomicity.&lt;/strong&gt; All three writes land, or none of them do. There is no universe where the card gets charged but the stock never gets reserved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation.&lt;/strong&gt; While the transaction is in flight, nobody else can see the half-finished middle. Another request checking the customer's balance will not catch a charge for an order that has not finished processing.&lt;/p&gt;

&lt;p&gt;You wrote the SQL. The database did the genuinely hard part, for free, and never asked for credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then you split the database, and the guarantee quietly leaves
&lt;/h2&gt;

&lt;p&gt;Traffic grows. Writes grow. That one box starts breathing heavily.&lt;/p&gt;

&lt;p&gt;So you do what everyone does. You shard, or you break the monolith into services and give each one its own database.&lt;/p&gt;

&lt;p&gt;The details vary. The outcome is identical: your data now lives on several independent machines that have never been introduced to each other.&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%2Fc0oyzabzlf6tky6mq9f7.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%2Fc0oyzabzlf6tky6mq9f7.png" alt="Diagram: on the left, one database wrapping charge, reserve and ledger in a single BEGIN and COMMIT; on the right, the same three steps landing in three separate databases on three separate machines, with the payment committed, the inventory write rejected, and the ledger never reached" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That one payment flow is now three separate operations against three separate databases.&lt;/p&gt;

&lt;p&gt;Card charged in the payments database. Stock reserved in the inventory database. Ledger entry recorded in accounting.&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;BEGIN&lt;/code&gt; big enough to cover all three, because none of them knows the others exist.&lt;/p&gt;

&lt;p&gt;So when the card charge commits and the inventory reservation then fails because the last unit just sold, nothing rolls back.&lt;/p&gt;

&lt;p&gt;That charge is already durable, on a different machine, in a database that has no idea an order died somewhere else.&lt;/p&gt;

&lt;p&gt;At a few hundred orders a day, you will see this maybe once and fix it by hand. At a few thousand transactions a second across distributed infrastructure, partial failure stops being an incident and starts being a Tuesday.&lt;/p&gt;

&lt;p&gt;This whole shape of problem has a name: the &lt;strong&gt;distributed transaction&lt;/strong&gt;. One logical operation spanning multiple independent databases or services, where every step needs to either succeed together or get cleaned up when something goes sideways.&lt;/p&gt;

&lt;p&gt;The literature offers you two answers. The industry has very firmly picked one of them, and knowing why will save you a genuinely painful quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The textbook answer: two-phase commit
&lt;/h2&gt;

&lt;p&gt;Two-phase commit is the classic academic solution, formalised decades ago in the &lt;a href="https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf" rel="noopener noreferrer"&gt;X/Open XA specification&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You introduce a new component, a &lt;strong&gt;coordinator&lt;/strong&gt;, whose entire job in life is to make every participant agree on the outcome before anybody makes anything permanent.&lt;/p&gt;

&lt;p&gt;It runs in two phases, which is where the name comes from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase one, prepare.&lt;/strong&gt; The coordinator asks every participant: can you commit this? Each database does the real work, durably records the change so a crash cannot lose it, locks the affected rows so nothing else can touch them, and then answers yes or no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase two, commit.&lt;/strong&gt; If anyone voted no, the coordinator tells everyone to abort and release their locks. If everyone voted yes, it tells everyone to commit, the locks come off, and you are done.&lt;/p&gt;

&lt;p&gt;What you get out of this is the same strong consistency you had with one database. Every participant agrees before anything is finalised, so there is no visible window where the system is half-done.&lt;/p&gt;

&lt;p&gt;On a whiteboard, this is exactly what you want. It is beautiful. It is also why almost nobody runs it across services.&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%2Fhsqb5oooagtd99o6tm9k.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%2Fhsqb5oooagtd99o6tm9k.png" alt="An Older Code Sir meme: an Imperial officer labelled " width="360" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Blocking is the whole problem
&lt;/h2&gt;

&lt;p&gt;2PC is a &lt;strong&gt;blocking protocol&lt;/strong&gt;, and blocking in a distributed system means you have made your availability the product of everybody else's uptime.&lt;/p&gt;

&lt;p&gt;Picture the coordinator collecting all three yes votes. Then, in the gap between collecting them and broadcasting the decision, it dies.&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%2Ffmk68j3ucjx5wp5hg8xa.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%2Ffmk68j3ucjx5wp5hg8xa.png" alt="Diagram: a coordinator box crossed out in red after collecting all votes, with dashed commit arrows that never arrive at the three participants below, each of which is prepared, has voted yes, and is still holding its row locks" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now look at the participants.&lt;/p&gt;

&lt;p&gt;They cannot commit on their own, because the coordinator might have been about to say abort.&lt;/p&gt;

&lt;p&gt;They cannot abort on their own either, because the coordinator might have been about to say commit, and one of their peers might have already gone through with it.&lt;/p&gt;

&lt;p&gt;So they wait. Holding locks. Indefinitely.&lt;/p&gt;

&lt;p&gt;And every other transaction in your system that wants to touch any of those rows is now queued behind a decision that nobody alive can make.&lt;/p&gt;

&lt;p&gt;Crashes are not even the interesting failure. The boring one is worse.&lt;/p&gt;

&lt;p&gt;A single slow participant stalls the entire transaction. If the ledger service takes ten seconds to answer the prepare message, the payment service and the inventory service both sit there holding locks for ten seconds, doing nothing productive whatsoever.&lt;/p&gt;

&lt;p&gt;Your whole system now moves at the speed of its slowest participant, which in a microservice estate is whichever team deployed most recently.&lt;/p&gt;

&lt;p&gt;Add a network partition and it gets worse still, because the coordinator cannot distinguish "the message did not arrive" from "the message arrived and the reply got lost". There is no safe default to pick.&lt;/p&gt;

&lt;p&gt;Pat Helland's &lt;a href="https://www.cidrdb.org/cidr2007/papers/cidr07p15.pdf" rel="noopener noreferrer"&gt;Life beyond Distributed Transactions&lt;/a&gt; made this argument in 2007 and it has aged extremely well. Distributed transactions across autonomous services do not work at internet scale, so build systems that do not need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where 2PC actually does live
&lt;/h2&gt;

&lt;p&gt;To be fair to the protocol, 2PC is alive and shipping. It is just not shipping where you would write it yourself.&lt;/p&gt;

&lt;p&gt;It lives &lt;strong&gt;inside&lt;/strong&gt; distributed databases. &lt;a href="https://research.google/pubs/spanner-googles-globally-distributed-database/" rel="noopener noreferrer"&gt;Google Spanner&lt;/a&gt; and &lt;a href="https://docs.yugabyte.com/preview/architecture/transactions/distributed-txns/" rel="noopener noreferrer"&gt;YugabyteDB&lt;/a&gt; both run two-phase commit under the hood to coordinate writes across shards.&lt;/p&gt;

&lt;p&gt;The difference is coupling. In there, the coordinator and the participants are components of one system, with one deployment, one failure model, one team, and a consensus protocol like Paxos or Raft underneath to make the coordinator itself fault-tolerant.&lt;/p&gt;

&lt;p&gt;Crucially, the database is absorbing that complexity so that you, the caller, never see it.&lt;/p&gt;

&lt;p&gt;The moment you try to rebuild that arrangement across independent services with different release trains, different on-call rotations and different opinions about timeouts, every assumption that makes it work in Spanner evaporates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sagas: give up atomicity, keep your weekends
&lt;/h2&gt;

&lt;p&gt;When companies genuinely need to coordinate work across services, the pattern they reach for is the &lt;a href="https://microservices.io/patterns/data/saga.html" rel="noopener noreferrer"&gt;saga&lt;/a&gt;. Uber, Netflix, Amazon and DoorDash all run this in production.&lt;/p&gt;

&lt;p&gt;Sagas start from a deliberately humbler assumption.&lt;/p&gt;

&lt;p&gt;You do not actually need all-or-nothing atomicity spanning five services. You need a way to reliably end up in a consistent state, even when a step blows up along the way.&lt;/p&gt;

&lt;p&gt;So instead of one big locked distributed transaction, you break the work into a chain of independent local transactions. Each service does its piece and commits to its own database, on its own terms, immediately.&lt;/p&gt;

&lt;p&gt;When something fails halfway down the chain, there is no rollback available, because the earlier steps are already committed somewhere else. Instead you run a &lt;strong&gt;compensating action&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is a business-level undo. A refund instead of a rollback. A cancellation instead of an abort.&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%2Fvjuifbiewlxqb2ioos6a.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%2Fvjuifbiewlxqb2ioos6a.png" alt="Diagram: three saga steps running left to right, card charged and stock reserved both committed, the ledger write rejected, then two orange compensating actions running right to left: release the stock, then refund the card" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you trade away is strong consistency. What you get is &lt;strong&gt;eventual consistency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The system can genuinely be inconsistent for a few seconds. The customer might see a charge land before they see the refund arrive.&lt;/p&gt;

&lt;p&gt;But it always converges, and nothing is blocked while it converges. Every other transaction in the system keeps flowing the entire time, because no saga step ever holds a lock that a stranger on another machine is responsible for releasing.&lt;/p&gt;

&lt;p&gt;That is the actual trade. Not "correct versus incorrect", but "briefly visible mess" versus "system-wide stall".&lt;/p&gt;

&lt;h2&gt;
  
  
  Choreography or orchestration
&lt;/h2&gt;

&lt;p&gt;Something has to notice the failure and kick off the compensations. Who that something is turns out to be the design decision that matters most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choreography&lt;/strong&gt; is the decentralised option. It is publish and subscribe: each service broadcasts an event when it finishes, and whoever cares picks it up.&lt;/p&gt;

&lt;p&gt;Payments charges the card and publishes &lt;code&gt;CardCharged&lt;/code&gt;. Inventory is listening, reserves the stock, publishes &lt;code&gt;StockReserved&lt;/code&gt;. Ledger picks that up and records the entry. On failure, a service publishes a failure event and upstream services run their own compensations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestration&lt;/strong&gt; is the centralised option. A dedicated orchestrator drives the flow, calling one service at a time and waiting for confirmation before moving on.&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%2F0m4x6okck6tkh9w1ziq1.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%2F0m4x6okck6tkh9w1ziq1.png" alt="Diagram: choreography on the left as a chain of services each reacting to the previous one's event with no central state, orchestration on the right as a single durable orchestrator fanning out to payments, inventory and ledger" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choreography is lovely at three steps. It is miserable at eight.&lt;/p&gt;

&lt;p&gt;Once a dozen services are publishing and reacting to each other, "where is order 1841 right now?" becomes an archaeology project. Which step failed? Which compensations already ran? Did the refund actually go through, or is it still retrying?&lt;/p&gt;

&lt;p&gt;Nobody owns the answer, so you go and read twelve services' logs to reconstruct it.&lt;/p&gt;

&lt;p&gt;Orchestration costs you a central component, and buys you the one thing choreography cannot give you: a single place that knows the state of every in-flight transaction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://temporal.io/" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt;, built by the people behind Uber's &lt;a href="https://cadenceworkflow.io/" rel="noopener noreferrer"&gt;Cadence&lt;/a&gt; workflow engine, and &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/sample-saga-pattern.html" rel="noopener noreferrer"&gt;AWS Step Functions&lt;/a&gt; both exist specifically to be that component.&lt;/p&gt;

&lt;p&gt;And here is the part that matters most, the reason the orchestrator is not just 2PC wearing a new hat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a saga orchestrator crashes, nothing is holding locks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is durable. It restarts, reads its own state back from its own database, and resumes from exactly where it stopped. Meanwhile the rest of your system never noticed, because no rows were ever frozen waiting on it.&lt;/p&gt;

&lt;p&gt;A dead 2PC coordinator freezes your data. A dead orchestrator just delays one workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compensating actions are where the honesty comes in
&lt;/h2&gt;

&lt;p&gt;"Just undo the previous step" sounds tidy. In practice, it is where sagas get their reputation.&lt;/p&gt;

&lt;p&gt;The card charge committed, the inventory reservation failed, so you refund. Fine. Except that refund is not the invisible cleanup a database rollback gives you.&lt;/p&gt;

&lt;p&gt;The customer sees a charge appear on their card. Then, seconds later, a refund. Their bank probably sent them a push notification for each one. It is correct, and it still generates a support ticket.&lt;/p&gt;

&lt;p&gt;Some actions are worse than visible. Some are simply not undoable.&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%2Fxdxurblz1ingdet6xkww.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%2Fxdxurblz1ingdet6xkww.png" alt="Hagrid meme captioned " width="360" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You cannot unsend an email. You can send a follow-up saying "ignore that last one", which is a different thing pretending to be the same thing.&lt;/p&gt;

&lt;p&gt;If you fired a webhook at a third party, you can send a cancellation, but you cannot make them process it in time, or at all.&lt;/p&gt;

&lt;p&gt;Every step in your saga needs a compensating action defined up front, and you should be clear-eyed that some of them are inherently imperfect.&lt;/p&gt;

&lt;p&gt;Then there is the recursive problem: &lt;strong&gt;compensations fail too&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What happens when the refund API is down at exactly the moment you need to refund? Now you need retries on your failure handling. And the instant you have retries, you need idempotency, otherwise a flaky network refunds the customer twice and you have invented a new bug that is much harder to explain in a postmortem.&lt;/p&gt;

&lt;p&gt;The trick is to derive the idempotency key from the thing you are undoing, never from the attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compensate_charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_cents&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# keyed on the order, not on this attempt, so ten retries of this
&lt;/span&gt;    &lt;span class="c1"&gt;# compensation collapse into exactly one refund on the customer's card
&lt;/span&gt;    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund:order:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;payment_intent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;lookup_payment_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&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;Generate a fresh UUID per attempt and you have an idempotency key that guarantees nothing.&lt;/p&gt;

&lt;p&gt;The uncomfortable summary is that your unhappy path needs the same reliability engineering as your happy path. Most teams budget for one of those.&lt;/p&gt;
&lt;h2&gt;
  
  
  The dual write problem, quietly waiting behind all of this
&lt;/h2&gt;

&lt;p&gt;Even with perfect compensation logic, there is one more failure mode that catches teams off guard, and it is the one I would bet money is already lurking in your codebase.&lt;/p&gt;

&lt;p&gt;When the payment service finishes charging the card, it has to do two things. Save the result to its own database, and publish an event so the next step knows to run.&lt;/p&gt;

&lt;p&gt;Those are two writes, to two different systems, with no shared transaction between 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%2Fd35ynn4cpksmrpu7c19u.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%2Fd35ynn4cpksmrpu7c19u.png" alt="Drowning High Five meme: one hand labelled " width="360" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the database write succeeds and the event publish fails, the next step never fires and the saga stalls silently. No error, no compensation, just an order sitting in &lt;code&gt;charged&lt;/code&gt; forever until a customer emails you.&lt;/p&gt;

&lt;p&gt;If the event publishes and the database write fails, downstream services are now reacting enthusiastically to something that never happened.&lt;/p&gt;

&lt;p&gt;The fix is the &lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;transactional outbox&lt;/a&gt;, and it is one of those patterns that feels like cheating the first time you see it.&lt;/p&gt;

&lt;p&gt;Stop treating the event as a separate write. Put it in the same database, in the same transaction, as the data.&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'charged'&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1841&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1841&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CardCharged'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"order_id":1841,"amount_cents":4999}'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One local transaction. Both rows commit, or neither does. The dual write is gone, because there is no longer a second write to fail.&lt;/p&gt;

&lt;p&gt;A separate background relay then reads that outbox table and publishes to the broker. It can tail the database's own transaction log via change data capture, which is what &lt;a href="https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html" rel="noopener noreferrer"&gt;Debezium&lt;/a&gt; does, or it can just poll the table on an interval, which is unglamorous and works fine at most scales.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Card charged successfully] --&amp;gt; B[BEGIN local transaction]
    B --&amp;gt; C[Write the payment row]
    C --&amp;gt; D[Write the event into the outbox table]
    D --&amp;gt; E[COMMIT: both rows, or neither]
    E --&amp;gt; F{How does the relay&amp;lt;br/&amp;gt;pick it up?}
    F --&amp;gt;|CDC, tail the WAL| G[Publish to the message broker]
    F --&amp;gt;|or just poll the table| G
    G --&amp;gt; H[Next saga step runs]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef db fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef out fill:#6ea8ff,stroke:#2f5fbf,color:#1a1a1a

    class A start
    class F decision
    class B,C,D,E db
    class G,H out&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Note that this gives you at-least-once delivery, not exactly-once. The relay can crash after publishing and before marking the row as sent, so the event goes out twice.&lt;/p&gt;

&lt;p&gt;Which brings us back to idempotency. Every consumer in your saga needs to handle seeing the same event twice, because it will.&lt;/p&gt;
&lt;h2&gt;
  
  
  So what should you actually build?
&lt;/h2&gt;

&lt;p&gt;Before either of these patterns, ask the question that people skip because it feels like a cop-out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need a distributed transaction at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you can draw your service boundaries so that data which changes together lives in the same database, do that. Move the inventory and ledger tables next to the payments table if they are always updated together.&lt;/p&gt;

&lt;p&gt;A local transaction is simpler, faster and more reliable than any distributed thing you can build on top of it. This is always the best answer when you can get away with it, and it is enormously easier to get right up front than to retrofit two years and forty services later.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[One operation spans several services] --&amp;gt; B{Can that data live&amp;lt;br/&amp;gt;in a single database?}
    B --&amp;gt;|Yes| C[Just use a local transaction]
    B --&amp;gt;|No| D{Is eventual consistency&amp;lt;br/&amp;gt;acceptable here?}
    D --&amp;gt;|No| E[Put it in a distributed SQL database&amp;lt;br/&amp;gt;Spanner or YugabyteDB]
    D --&amp;gt;|Yes| F{Many steps, branching,&amp;lt;br/&amp;gt;or tricky compensations?}
    F --&amp;gt;|No| G[Saga with choreography]
    F --&amp;gt;|Yes| H[Saga with orchestration]
    H --&amp;gt; I[Temporal or AWS Step Functions]
    G --&amp;gt; J[Transactional outbox&amp;lt;br/&amp;gt;+ idempotent consumers]
    H --&amp;gt; J

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef good fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef alt fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef tool fill:#6ea8ff,stroke:#2f5fbf,color:#1a1a1a

    class A start
    class B,D,F decision
    class C,H good
    class G,E alt
    class I,J tool&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If you truly cannot keep it in one database, you are writing a saga. That part is not really up for debate anymore.&lt;/p&gt;

&lt;p&gt;The remaining question is which flavour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choreography&lt;/strong&gt; if the flow is short, the services are genuinely independent, and nobody needs a central view of where a transaction stands. An order placed event triggering an email and a push notification is a perfect fit, and wiring up an orchestrator for it would be theatre.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestration&lt;/strong&gt; for anything with branching logic, more than a handful of steps, or compensation logic you would rather define in one file than scatter across six repos. Most teams end up here eventually, and the tooling has got good enough that starting here is no longer expensive.&lt;/p&gt;

&lt;p&gt;And if eventual consistency is genuinely unacceptable for some specific slice of your data, put that slice in a distributed database that handles strong consistency internally. That is a completely different proposition from hand-rolling 2PC across services you do not control.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape that keeps winning
&lt;/h2&gt;

&lt;p&gt;Strip it all back and the production answer at basically every company operating at scale looks the same.&lt;/p&gt;

&lt;p&gt;A saga, orchestrated by something durable. Idempotent operations at every step, so retries are always safe. A transactional outbox, so events are as reliable as the database writes they describe.&lt;/p&gt;

&lt;p&gt;It means accepting that your system is briefly, visibly inconsistent sometimes.&lt;/p&gt;

&lt;p&gt;That is not a compromise the industry stumbled into. It is one it chose, on purpose, after watching two-phase commit deadlock enough production databases to make the decision for everyone.&lt;/p&gt;

&lt;p&gt;The database gave you atomicity for free. The moment you split it, the bill came due, and sagas are simply the cheapest way anyone has found to pay it.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;&lt;br&gt;
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.&lt;/em&gt;&lt;/p&gt;
&lt;em&gt;

&lt;p&gt;I'm building &lt;strong&gt;LiveReview&lt;/strong&gt;, a blast-radius aware AI code review built for your business-critical systems.&lt;/p&gt;

&lt;p&gt;Instead of presenting every diff with equal emphasis, &lt;strong&gt;LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spend code review effort where business risk is highest — not spread evenly across every diff.&lt;/p&gt;

&lt;p&gt;⭐ Star it on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/HexmosTech" rel="noopener noreferrer"&gt;
        HexmosTech
      &lt;/a&gt; / &lt;a href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;
        LiveReview
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Blast-Radius Aware AI Code Review for Business-Critical Systems
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/png/logo-with-text.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fpng%2Flogo-with-text.png" alt="LiveReview" height="80"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml" rel="noopener noreferrer"&gt;&lt;img alt="gitleaks.yml" title="gitleaks.yml: Secret scanning workflow" src="https://github.com/HexmosTech/LiveReview/actions/workflows/gitleaks.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml" rel="noopener noreferrer"&gt;&lt;img alt="osv-scanner.yml" title="osv-scanner.yml: Dependency vulnerability scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/osv-scanner.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml" rel="noopener noreferrer"&gt;&lt;img alt="govulncheck.yml" title="govulncheck.yml: Go vulnerability check" src="https://github.com/HexmosTech/LiveReview/actions/workflows/govulncheck.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml" rel="noopener noreferrer"&gt;&lt;img alt="semgrep.yml" title="semgrep.yml: Static analysis security scan" src="https://github.com/HexmosTech/LiveReview/actions/workflows/semgrep.yml/badge.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/gfx/dependabot-enabled.svg"&gt;&lt;img alt="dependabot-enabled" title="dependabot-enabled: Automated dependency updates are enabled" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fgfx%2Fdependabot-enabled.svg"&gt;&lt;/a&gt;&amp;nbsp;&lt;a href="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml" rel="noopener noreferrer"&gt;&lt;img alt="mcp-testcases.yml" title="mcp-testcases.yml: MCP integration test suite" src="https://github.com/HexmosTech/LiveReview/actions/workflows/mcp-testcases.yml/badge.svg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LiveReview: Risk-Aware AI Code Review for Business-Critical Systems&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;LiveReview is an AI code reviewer that scores every hunk of a diff by &lt;strong&gt;blast radius&lt;/strong&gt;: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.&lt;/p&gt;


  
    
    &lt;span class="m-1"&gt;blast-radius-demo.mp4&lt;/span&gt;
  

  

  


&lt;p&gt;&lt;i&gt;LiveReview's Blast Radius &amp;amp; Review Priority scoring, live in the diff viewer.&lt;/i&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;The exact math, not a black box&lt;/th&gt;

&lt;th&gt;Visualize blast radius at a glance&lt;/th&gt;

&lt;th&gt;Every factor that feeds the score&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-3.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-3.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-4.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-4.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/HexmosTech/LiveReview/./assets/screenshots/blast-radius/new-risk-score-2.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2FLiveReview%2FHEAD%2F.%2Fassets%2Fscreenshots%2Fblast-radius%2Fnew-risk-score-2.webp" width="280"&gt;&lt;/a&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

How does Blast Radius scoring work? (a more technical explanation)

&lt;p&gt;&lt;strong&gt;Here's the goal:&lt;/strong&gt;&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.&lt;/li&gt;

&lt;li&gt;A 300-line UI change in one file, fully covered by tests…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/HexmosTech/LiveReview" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;b&gt;Click below to try LiveReview with your codebase:&lt;/b&gt;&lt;/p&gt;

&lt;/em&gt;&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hexmos.com/livereview" rel="noopener noreferrer"&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%2Fvls0pq7nymbrll98je6s.png" alt="LiveReview Banner" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>microservices</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
