<?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: street food Chinese</title>
    <description>The latest articles on DEV Community by street food Chinese (@street_foodchinese_57726).</description>
    <link>https://dev.to/street_foodchinese_57726</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%2F4180547%2F277eb941-462d-4b6a-84e3-ad673435a643.png</url>
      <title>DEV Community: street food Chinese</title>
      <link>https://dev.to/street_foodchinese_57726</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/street_foodchinese_57726"/>
    <language>en</language>
    <item>
      <title>The Uninstall That Actually Works: A Practitioner’s Guide to Cleaning Up Windows Apps</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:08:10 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/the-uninstall-that-actually-works-a-practitioners-guide-to-cleaning-up-windows-apps-24o9</link>
      <guid>https://dev.to/street_foodchinese_57726/the-uninstall-that-actually-works-a-practitioners-guide-to-cleaning-up-windows-apps-24o9</guid>
      <description>&lt;h1&gt;
  
  
  The Uninstall That Actually Works: A Practitioner’s Guide to Cleaning Up Windows Apps
&lt;/h1&gt;

&lt;p&gt;I’ve lost count of how many times a “simple” uninstall left me with a broken PATH, a zombie service, or a registry entry that made my next install fail. The Windows “Add or Remove Programs” panel is a polite fiction — it removes the files the installer &lt;em&gt;wants&lt;/em&gt; to remove, not the ones it &lt;em&gt;should&lt;/em&gt;. After years of fighting leftover DLLs and orphaned scheduled tasks, I’ve built a repeatable, paranoid workflow for clean uninstalls. Here’s what actually works, including the failure modes I’ve hit and the exact commands I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Inventory Before You Nuke
&lt;/h2&gt;

&lt;p&gt;The first mistake is clicking “Uninstall” without knowing what you’re dealing with. A clean uninstall starts with a full inventory of the app’s footprint. I do this on every machine, even for small utilities.&lt;/p&gt;

&lt;p&gt;Open an elevated PowerShell (Admin) and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all installed apps with their uninstall strings&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DisplayVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UninstallString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;InstallLocation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*YourAppName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Format-List&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Also check per-user installs (common for MSIX or per-user MSI)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UninstallString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*YourAppName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Format-List&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me the official uninstall string. But that’s just the starting point. I also check for services, scheduled tasks, and drivers that the app might have registered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Services and scheduled tasks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*YourAppName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*YourAppName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*YourAppName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ve seen apps register a Windows service under a completely different name (e.g., “UpdateAgent” for a PDF reader). So I also grep the registry for the app’s vendor name or a unique file path. It’s tedious, but it beats a half-removed app that breaks your next install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Run the Official Uninstaller, Then Hunt the Leftovers
&lt;/h2&gt;

&lt;p&gt;Run the official uninstaller first — always. For MSI-based apps, use the &lt;code&gt;/x&lt;/code&gt; flag with the product code or the &lt;code&gt;/uninstall&lt;/code&gt; flag. For most apps, you can invoke the &lt;code&gt;UninstallString&lt;/code&gt; directly from the registry. Here’s a safe way to do it programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the uninstaller silently (if supported) — replace with your app's string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$uninstallString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourAppName"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UninstallString&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ArgumentList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/c &lt;/span&gt;&lt;span class="nv"&gt;$uninstallString&lt;/span&gt;&lt;span class="s2"&gt; /quiet /norestart"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Wait&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here’s the thing: the official uninstaller is &lt;em&gt;not&lt;/em&gt; the end. After it finishes, I immediately check what’s left. The most common leftovers I’ve seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registry keys&lt;/strong&gt; under &lt;code&gt;HKCU\Software\VendorName&lt;/code&gt; or &lt;code&gt;HKLM\Software\WOW6432Node\VendorName&lt;/code&gt; (for 32-bit apps on 64-bit systems).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProgramData folders&lt;/strong&gt; — apps love to cache updates there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AppData\Local and AppData\Roaming&lt;/strong&gt; — config files, logs, and crash dumps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driver store entries&lt;/strong&gt; for kernel drivers (these are the worst — they persist even after reboot).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a quick sweep I run after the uninstaller exits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check common leftover locations&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$paths&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;ProgramFiles&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;${env:ProgramFiles(x86)}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;ProgramData&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;APPDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*YourAppName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Check the registry for vendor keys&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU:\Software"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSChildName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*VendorName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\Software"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSChildName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*VendorName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\Software\WOW6432Node"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSChildName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*VendorName*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find anything, don’t delete it yet. Rename it first (e.g., append &lt;code&gt;.bak&lt;/code&gt;). This lets you test if the app actually needed it — if your system works fine for a week, then delete the backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The Failure Modes That Bite You
&lt;/h2&gt;

&lt;p&gt;I’ve hit three distinct failure modes that make a “clean” uninstall a lie. Here’s how to handle each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: The “silent” uninstaller that isn’t.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Many apps claim &lt;code&gt;/quiet&lt;/code&gt; support, but they actually spawn a GUI or a UAC prompt that hangs your script. My fix: run the uninstaller interactively first, watch what it does, then script it. If you must script it, use a timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$proc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$uninstallString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ArgumentList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/quiet"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PassThru&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Wait&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExitCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Write-Warning&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Uninstaller returned &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExitCode&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 2: Leftover services that auto-start on boot.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
I once had an app that left a service pointing to a deleted executable. The service failed to start, but the error kept popping up in Event Viewer. The fix is to remove the service &lt;em&gt;after&lt;/em&gt; the uninstaller runs, because the uninstaller might try to stop it and fail. Here’s how I remove a stubborn service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop and delete a service (run as Admin)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$svcName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourAppService"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Stop-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$svcName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;sc.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$svcName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;sc.exe delete&lt;/code&gt; is more reliable than &lt;code&gt;Remove-Service&lt;/code&gt; in older PowerShell versions. Also, check &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Services&lt;/code&gt; for any keys that reference the app’s path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: The reboot requirement.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Some apps (especially driver installers) need a reboot to finish removal. If you skip the reboot, the driver files remain in &lt;code&gt;C:\Windows\System32\drivers&lt;/code&gt; and can cause conflicts. My rule: if the uninstaller says “reboot required,” reboot &lt;em&gt;before&lt;/em&gt; you clean up leftover files. Otherwise, the system might still be using the files you’re about to delete, and you’ll get access denied errors.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Manual Cleanup — The “Nuclear” Pass
&lt;/h2&gt;

&lt;p&gt;After the official uninstaller and the service cleanup, I do a manual pass. This is where I delete the leftovers I found in Step 2. But I’m careful about what I delete. Here’s my priority list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AppData folders&lt;/strong&gt; — safe to delete if the app is gone and you don’t care about settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProgramData folders&lt;/strong&gt; — check for shared components; if another app uses them, leave them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry keys&lt;/strong&gt; — only delete vendor keys that you’ve confirmed are orphaned. Use &lt;code&gt;reg.exe delete&lt;/code&gt; or &lt;code&gt;Remove-Item&lt;/code&gt; in PowerShell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driver files&lt;/strong&gt; — only after reboot, and only if you’re sure no other device uses them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For registry cleanup, I use a targeted approach. I don’t blindly delete entire vendor trees because some apps share them (e.g., Adobe or Microsoft). Here’s a safe snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Backup the registry key first, then remove it&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$keyPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU:\Software\YourVendor\YourApp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keyPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;reg.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keyPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="s2"&gt;\YourApp_registry_backup.reg"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/y&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keyPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backup is non-negotiable. I’ve had to restore a registry key after discovering that a shared DLL was still being referenced by another app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify the Uninstall Is Actually Clean
&lt;/h2&gt;

&lt;p&gt;The final step is verification. I don’t trust the uninstaller’s exit code. I check the system state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify no traces remain&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$appName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourAppName"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$checks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\Software\YourVendor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"HKCU:\Software\YourVendor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;ProgramFiles&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;ProgramData&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;APPDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$checks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*&lt;/span&gt;&lt;span class="nv"&gt;$appName&lt;/span&gt;&lt;span class="s2"&gt;*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*&lt;/span&gt;&lt;span class="nv"&gt;$appName&lt;/span&gt;&lt;span class="s2"&gt;*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leftover found: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSPath&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also check the Start Menu shortcuts and the Windows “Apps” list via &lt;code&gt;Get-AppxPackage&lt;/code&gt; if it was a Store app. If everything comes back empty, I consider the uninstall clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;If you’re dealing with a specific command-line tool that’s hard to uninstall because it ships as a single binary with no installer, the process is a bit different — you’re just deleting the folder and any config in your user profile. I recently used &lt;a href="https://gitvaults.com/download/2026/01/12/hemmeligorg-hemmeligapp-cli-v1-0-2/en" rel="noopener noreferrer"&gt;hemmeligorg hemmeligapp cli v1 0 2&lt;/a&gt; from GitVaults, and the uninstall was refreshingly simple: delete the executable, remove the &lt;code&gt;.config&lt;/code&gt; file from &lt;code&gt;%APPDATA%&lt;/code&gt;, and you’re done. GitVaults is a good source for Windows tools that don’t pollute your system with registry entries — worth checking if you prefer apps that respect your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;This workflow is based on my experience with Windows 10/11 and common MSI, NSIS, and custom installers. Your mileage may vary — some enterprise apps use custom installation frameworks that hide files in odd places, and some drivers are notoriously sticky. If you spot a step that’s wrong or a better technique, I’d genuinely like to hear it. Corrections and edge cases from real-world usage are what make this guide better.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>software</category>
      <category>tools</category>
    </item>
    <item>
      <title>Mirror Failover: When Your "Redundant" Setup Isn't Actually Redundant</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:05:45 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/mirror-failover-when-your-redundant-setup-isnt-actually-redundant-4n59</link>
      <guid>https://dev.to/street_foodchinese_57726/mirror-failover-when-your-redundant-setup-isnt-actually-redundant-4n59</guid>
      <description>&lt;h1&gt;
  
  
  Mirror Failover: When Your "Redundant" Setup Isn't Actually Redundant
&lt;/h1&gt;

&lt;p&gt;I've spent the last decade running storage systems for small-to-mid-sized businesses, and if there's one lesson I've learned the hard way, it's this: a mirror without a tested failover path is just a more expensive way to lose data. You'd think that having two disks, two controllers, or two nodes means you're safe. Then the day comes when your primary silently degrades, and you discover your "automatic" failover was never wired to the thing that actually broke.&lt;/p&gt;

&lt;p&gt;Let me walk you through what mirror failover really looks like in practice—the commands, the failure modes, and the edge cases that documentation glosses over.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Difference Between "Mirrored" and "Highly Available"
&lt;/h2&gt;

&lt;p&gt;First, a distinction that matters. A RAID-1 mirror is not the same as a failover cluster. RAID-1 gives you redundancy at the block level—if one disk dies, the other still has your data. But your operating system, your file system, and your application are still running on whatever controller was active. If that controller hangs, you've got a paperweight with a healthy disk inside.&lt;/p&gt;

&lt;p&gt;True mirror failover means that when the active path goes away, a standby path takes over &lt;em&gt;without human intervention&lt;/em&gt; and &lt;em&gt;without data loss&lt;/em&gt;. That requires three things working together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A detection mechanism (heartbeat, SCSI reservation, or network ping)&lt;/li&gt;
&lt;li&gt;A promotion mechanism (the standby takes the active role)&lt;/li&gt;
&lt;li&gt;A quorum or fencing rule (so you don't get split-brain—two nodes both thinking they're primary)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've seen more split-brain incidents than I care to count. The classic symptom: both nodes write to the same LUN, and now you have two divergent versions of your database. That's not failover; that's corruption with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Failover Workflow (Linux + DRBD)
&lt;/h2&gt;

&lt;p&gt;Let me show you a real setup I use for a small file server. It's DRBD (Distributed Replicated Block Device) over two nodes, with a floating IP and a simple heartbeat script. Nothing fancy, but it works.&lt;/p&gt;

&lt;p&gt;Here's the DRBD resource config, &lt;code&gt;/etc/drbd.d/r0.res&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;r0&lt;/span&gt; {
    &lt;span class="n"&gt;protocol&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;;
    &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;node1&lt;/span&gt; {
        &lt;span class="n"&gt;device&lt;/span&gt;    /&lt;span class="n"&gt;dev&lt;/span&gt;/&lt;span class="n"&gt;drbd0&lt;/span&gt;;
        &lt;span class="n"&gt;disk&lt;/span&gt;      /&lt;span class="n"&gt;dev&lt;/span&gt;/&lt;span class="n"&gt;sdb1&lt;/span&gt;;
        &lt;span class="n"&gt;address&lt;/span&gt;   &lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;168&lt;/span&gt;.&lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;11&lt;/span&gt;:&lt;span class="m"&gt;7788&lt;/span&gt;;
        &lt;span class="n"&gt;meta&lt;/span&gt;-&lt;span class="n"&gt;disk&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;;
    }
    &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;node2&lt;/span&gt; {
        &lt;span class="n"&gt;device&lt;/span&gt;    /&lt;span class="n"&gt;dev&lt;/span&gt;/&lt;span class="n"&gt;drbd0&lt;/span&gt;;
        &lt;span class="n"&gt;disk&lt;/span&gt;      /&lt;span class="n"&gt;dev&lt;/span&gt;/&lt;span class="n"&gt;sdb1&lt;/span&gt;;
        &lt;span class="n"&gt;address&lt;/span&gt;   &lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;168&lt;/span&gt;.&lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;12&lt;/span&gt;:&lt;span class="m"&gt;7788&lt;/span&gt;;
        &lt;span class="n"&gt;meta&lt;/span&gt;-&lt;span class="n"&gt;disk&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After you've initialized both sides with &lt;code&gt;drbdadm create-md r0&lt;/code&gt; and brought them up, the primary is set manually. But for failover, you want a script that does this on node2 when node1 goes silent:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/failover.sh - run on the standby node&lt;/span&gt;

&lt;span class="nv"&gt;DRBD_RESOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"r0"&lt;/span&gt;
&lt;span class="nv"&gt;FLOATING_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.10.20"&lt;/span&gt;
&lt;span class="nv"&gt;PEER_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.10.11"&lt;/span&gt;
&lt;span class="nv"&gt;PEER_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"7788"&lt;/span&gt;

&lt;span class="c"&gt;# Check if peer is alive&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; nc &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; 3 &lt;span class="nv"&gt;$PEER_IP&lt;/span&gt; &lt;span class="nv"&gt;$PEER_PORT&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;logger &lt;span class="s2"&gt;"Peer &lt;/span&gt;&lt;span class="nv"&gt;$PEER_IP&lt;/span&gt;&lt;span class="s2"&gt; unreachable, promoting DRBD resource"&lt;/span&gt;

    &lt;span class="c"&gt;# Promote to primary&lt;/span&gt;
    drbdadm primary &lt;span class="nv"&gt;$DRBD_RESOURCE&lt;/span&gt;

    &lt;span class="c"&gt;# Mount the filesystem&lt;/span&gt;
    mount /dev/drbd0 /mnt/data

    &lt;span class="c"&gt;# Bring up the floating IP&lt;/span&gt;
    ip addr add &lt;span class="nv"&gt;$FLOATING_IP&lt;/span&gt;/24 dev eth0
    arping &lt;span class="nt"&gt;-c&lt;/span&gt; 3 &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; eth0 &lt;span class="nv"&gt;$FLOATING_IP&lt;/span&gt;

    logger &lt;span class="s2"&gt;"Failover complete, floating IP &lt;/span&gt;&lt;span class="nv"&gt;$FLOATING_IP&lt;/span&gt;&lt;span class="s2"&gt; is now local"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, here's the catch. That &lt;code&gt;nc -z&lt;/code&gt; check is a single point of failure. What if node1 is alive but its network path to node2 is down? Then node2 promotes, and node1 is still running—now you have two primaries. That's split-brain. The fix is a fencing mechanism: a STONITH device (Shoot The Other Node In The Head), or at minimum, a shared quorum disk that only one node can hold a reservation on.&lt;/p&gt;

&lt;p&gt;I learned this the hard way when a switch port died, and both nodes decided they were primary. The DRBD metadata got confused, and I had to manually resolve the conflict with &lt;code&gt;drbdadm disconnect r0&lt;/code&gt; and then &lt;code&gt;drbdadm -- --force primary r0&lt;/code&gt; on the side I trusted. Not fun at 2 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Modes You Won't Find in the Brochure
&lt;/h2&gt;

&lt;p&gt;Let me list the ones that actually bite:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent I/O errors.&lt;/strong&gt; The disk is still spinning, but it returns read errors. DRBD marks the peer as "Outdated" only after a certain number of failures. If your application doesn't check for read errors, you might be serving corrupt data for hours before anyone notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale heartbeat.&lt;/strong&gt; Your failover script checks a port, but the service behind that port is hung. The port is open, the process is dead. Your script says "all good," but your users see timeouts. You need to check &lt;em&gt;application-level&lt;/em&gt; health, not just TCP connectivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial network partition.&lt;/strong&gt; Node1 can talk to node2, but node2 can't talk to node1. Both see each other as down. This is where quorum rules matter. With two nodes, you have no majority. I recommend a tie-breaker: a third node that acts as a witness, or a shared disk that only one node can lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failover during a write.&lt;/strong&gt; This is the nastiest one. If node1 crashes mid-write, and node2 promotes, node2's DRBD may have the data in its write-ahead log but not yet flushed to disk. Protocol C (synchronous) guarantees that a write is acknowledged only after both nodes have it in memory. But if node1 dies &lt;em&gt;before&lt;/em&gt; acknowledging, node2 might have the data but not the acknowledgment. In that case, you must decide: serve the write (and risk losing it if node2 also dies) or reject it. I always choose to serve it, because a partial write is better than a full outage—but you need to know your application can handle a torn write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PowerShell Side: Windows Storage Spaces and Failover Clustering
&lt;/h2&gt;

&lt;p&gt;Not everyone is on Linux. On Windows, I've used Storage Spaces with a two-node failover cluster. The setup is different, but the principles are the same. Here's a snippet to check the health of a cluster role and force a move if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check cluster health and force failover if needed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$cluster&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Cluster&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FileCluster01"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ClusterGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Cluster&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cluster&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FileServer"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsStateful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Role is not stateful, checking node status..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ClusterNode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Cluster&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cluster&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Up"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; is down. Attempting to move role."&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Move-ClusterGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FileServer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="kr"&gt;break&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Role is currently owned by: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OwnerNode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edge case here is that &lt;code&gt;Move-ClusterGroup -Force&lt;/code&gt; can cause a brief outage if the storage is not in a healthy state. I've seen it fail with "The cluster resource could not be brought online because the dependent resource is not available." That usually means the disk resource is still marked as "Pending" or "Failed" after the node came back. The fix is to manually reset the disk resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ClusterResource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cluster Disk 1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Set-ClusterResource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Offline&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ClusterResource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cluster Disk 1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Set-ClusterResource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Online&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the thing: if the disk is actually corrupt, that command will just bring it online into a corrupted state. Always run &lt;code&gt;chkdsk /f&lt;/code&gt; on the underlying volume first, &lt;em&gt;before&lt;/em&gt; you re-add it to the cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Case: Failover When the Primary Is Not Dead
&lt;/h2&gt;

&lt;p&gt;Here's a scenario that stumps most people. The primary node is alive, the network is fine, but the &lt;em&gt;application&lt;/em&gt; has crashed. In a traditional mirror setup, your failover script only triggers on node loss. So your app is down, but your mirror is perfectly healthy. You're not redundant; you're just backed up.&lt;/p&gt;

&lt;p&gt;The fix is a health-check loop that monitors the application, not just the node. On Linux, that might be a systemd timer that checks if your NFS export is responding. On Windows, you'd use a resource monitor in the cluster that runs a custom DLL or PowerShell script. The script should return non-zero if the app is unresponsive, and the cluster will then fail over the role.&lt;/p&gt;

&lt;p&gt;I once had a customer whose PostgreSQL instance was hung due to a deadlock. The node was up, the network was up, but the database was unresponsive. Their failover script didn't check the database, so the mirror stayed passive. They lost six hours of availability because no one had thought to test "what happens when the app hangs, not the OS."&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Failover (Do It Now, Not at 3 AM)
&lt;/h2&gt;

&lt;p&gt;You cannot test failover in production. You can, however, test it in a staging environment that mirrors your production topology. Here's my minimum test suite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kill the primary node&lt;/strong&gt; (power off, not graceful shutdown). Verify the standby promotes within the expected time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unplug the network cable&lt;/strong&gt; from the primary. Verify the standby promotes but &lt;em&gt;does not&lt;/em&gt; cause split-brain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill the application process&lt;/strong&gt; on the primary, not the node. Verify the health check catches it and fails over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a file to the primary, then immediately kill it.&lt;/strong&gt; Verify the file is present on the standby after failover, and that its content is intact.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of these tests fail, you have a bug in your failover logic. Fix it before you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;If you're dealing with Windows-based tools and need a reliable terminal emulator that handles multiple sessions cleanly—especially when you're juggling cluster nodes and storage consoles—check out &lt;a href="https://gitvaults.com/download/2026/09/23/ys-ll-uniterm-1-9-5/en" rel="noopener noreferrer"&gt;ys ll uniterm 1 9 5&lt;/a&gt; on GitVaults. It's a solid utility for keeping your failover sessions organized without the bloat of full IDE environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;I've written this from my own experience, and I'm sure I've missed some corner cases or made assumptions that don't hold in your environment. If you spot an error, or if your failover setup handles something differently, please correct me. The whole point of this article is to get you thinking about what can break—so if you've seen a failure mode I didn't cover, I'd genuinely like to hear about it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>The CLI vs GUI Debate: What I Actually Use When the Network Breaks</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:05:39 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/the-cli-vs-gui-debate-what-i-actually-use-when-the-network-breaks-872</link>
      <guid>https://dev.to/street_foodchinese_57726/the-cli-vs-gui-debate-what-i-actually-use-when-the-network-breaks-872</guid>
      <description>&lt;h1&gt;
  
  
  The CLI vs GUI Debate: What I Actually Use When the Network Breaks
&lt;/h1&gt;

&lt;p&gt;绑 I’ve been running Windows-based IT infrastructure for over a decade, and I’ve lost count of how many times I’ve had to explain why I still open a terminal when there’s a perfectly good GUI sitting right there. The truth is, the CLI vs GUI argument isn’t about which is “better” in some abstract sense. It’s about what gets the job done when the job is ugly, time-sensitive, and full of edge cases that the GUI designers never imagined. This article is my practical, first-person take on when I reach for PowerShell, when I reach for a window, and why I often end up using both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow: When the GUI Lies to You
&lt;/h2&gt;

&lt;p&gt;Let me set the scene. A client calls at 3 PM on a Tuesday: “The file server is slow, and nobody can map the new drive.” I remote in, open the Server Manager GUI, and what do I see? Everything looks green. The disks show healthy, the services show running, and the event log shows… well, it shows a wall of warnings that the GUI’s “summary” page conveniently filters out)Skip. The GUI is designed to reassure you, not to troubleshoot. It hides the noise, and in a crisis, the noise is the signal.&lt;/p&gt;

&lt;p&gt;So I open PowerShell. Not because I’m a purist, but because I need to see what the GUI is hiding. My first command is almost always this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-SmbConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ServerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FILESRV01"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ClientComputerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ServerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ShareName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells me who’s actually connected, with what status, and to which share. The GUI would show me the same list, but it would take three clicks and a refresh, and it wouldn’t show me the &lt;em&gt;status&lt;/em&gt; column in the same raw format. More importantly, I can pipe that output into a file, filter it, and compare it against a baseline from last week. The GUI gives you a snapshot; the CLI gives you a dataset.&lt;/p&gt;

&lt;p&gt;The real workflow difference is this: with a GUI, I’m navigating a tree of options that someone else decided was important. With a CLI, I’m composing a query that answers my exact question. When I need to know “which sessions have been idle for more than 30 minutes,” I can write that in one line. In the GUI, I’d have to export to CSVcars, open Excel, and filter manually. That’s not a “power user” flex; that’s just saving ten minutes on a bad day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failure Modes: Where Each Tool Cracks
&lt;/h2&gt;

&lt;p&gt;I’ve seen both tools fail in spectacular wayscars, and knowing those failure modes is half the battle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GUI failure mode: the silent hang.&lt;/strong&gt; You click “Apply” on a network adapter setting, and the window freezes. No error, no progress bar, just a spinning cursor. You wait, you wait, you alt-tab, you wait. Eventually you kill the process, and you’re left with a half-applied configuration that you can’t even see. The GUI has no transactional model. It either applies everything or nothing, and you don’t get to choose which.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI failure mode: the typo that nukes something.&lt;/strong&gt; I once ran a script that was supposed to remove stale user profiles, but I fat-fingered the path variable. Instead of &lt;code&gt;C:\Users\OldUsers&lt;/code&gt;, it hit &lt;code&gt;C:\Users&lt;/code&gt;. The command ran, and I watched the output scroll past, and I realized what I’d done about three seconds too late. The GUI would have asked me “Are you sure you want to delete 400 folders?” The CLI assumes you know what you’re doing, and sometimes you don’t.&lt;/p&gt;

&lt;p&gt;But here’s the thing: the CLI’s failure mode is recoverable. I can check the recycle bin (if I was smart enough to use &lt;code&gt;-WhatIf&lt;/code&gt; first, which I wasn’t that day), or I can restore from backup. The GUI’s failure mode is often a black hole. You don’t know what state the system is in, and the GUI won’t tell you until you close and reopen it, and by then the state has changed again.&lt;/p&gt;

&lt;p&gt;I’ve learned to build guardrails into my CLI usage. For anything destructive, I use &lt;code&gt;-WhatIf&lt;/code&gt; or &lt;code&gt;-Confirm&lt;/code&gt; flags, and I write the output to a log file before I execute. That’s the discipline that makes the CLI safe. The GUI gives you a false sense of safety because it looks friendly; the CLI gives you a real sense of danger because you can see the raw command, and that makes you cautious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Cases: When the GUI Can’t Even Open
&lt;/h2&gt;

&lt;p&gt;Here’s a scenario that happens more often than you’d think: the GUI itself won’t start. Maybe the display driver is broken, maybe the service that hosts the MMC snap-ins is stuck, maybe you’re remoted into a headless server that only has a command line. In those moments, the CLI isn’t a choice; it’s the only option.&lt;/p&gt;

&lt;p&gt;I remember a specific incident where a client’s server had a corrupted Windows event log service. The Event Viewer GUI would open, then immediately crash with a “0xc0000005” access violation. No amount of clicking “repair” would help. But I could still run PowerShell, and I could still query the event log directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-WinEvent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilterHashtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;LogName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'System'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Level&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="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;StartTime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;AddHours&lt;/span&gt;&lt;span class="err"&gt;(-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TimeCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ProviderName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-Table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AutoSize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me the exact error codes I needed to identify the corrupted log file, stop the service, rename the file, and restart the service. The GUI was completely useless, but the CLI saved the day. And that’s not an exotic edge case; it’s a Tuesday.&lt;/p&gt;

&lt;p&gt;Another edge case: automation. If I need to configure 50 workstations with the same network settings, I’m not going to click through the GUI 50 times. I’m going to write a script. The GUI is inherently interactive, which means it’s inherently slow. The CLI is inherently scriptable, which means it’s inherently repeatable. And repeatability is what turns a one-off fix into a standard operating procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Reality: Why I Use Both
&lt;/h2&gt;

&lt;p&gt;I’m not here to tell you to abandon the GUI. That would be stupid. The GUI is great for discovery, for learning, for seeing the shape of a system you’ve never touched before. When I’m on a new server, I open the GUI first. I look at the disk layout, the network adapters, the installed roles. That visual overview is worth something. It gives me context that a command list doesn’t.&lt;/p&gt;

&lt;p&gt;But once I know what I’m looking at, I switch to the CLI for the actual work. The GUI is for reading; the CLI is for writing. And that’s a useful mental model. When you’re “reading” a system, you want high-level, summarized, friendly views. When you’re “writing” changes, you want precision, repeatability, and auditability. The GUI gives you the former; the CLI gives you the latter.&lt;/p&gt;

&lt;p&gt;I also use the GUI to generate the CLI commands. PowerShell’s &lt;code&gt;Get-Command&lt;/code&gt; and &lt;code&gt;Get-Help&lt;/code&gt; are great, but sometimes I don’t know the exact cmdlet name. So I’ll do the action in the GUI, then check the PowerShell history or the event log to see what it actually did. That’s a reverse-engineering workflow that has saved me countless hours of guessing. The GUI is a teacher; the CLI is a tool.&lt;/p&gt;

&lt;p&gt;One more practical tip: I keep a “cheat sheet” of my most-used CLI commands in a text file on every server I manage. It’s not a script; it’s just a list of one-liners that I know work. When the GUI is down and I’m under pressure, I don’t want to be typing from memory. I want to copy-paste from a file that I’ve already tested. That’s the real advantage of the CLI: you can version-control your knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;If you’re like me and you spend a lot of time in the terminal, you’ll appreciate tools that make the CLI more accessible. I’ve been using a small utility called SubnetDesk to quickly calculate subnets and IP ranges without leaving the command line. It’s not a replacement for PowerShell, but it’s a nice addition to the toolkit. You can find it on GitVaults, a Windows software catalog that actually lists the files you can download, not just marketing pages. Here’s the direct link: &lt;a href="https://gitvaults.com/download/2026/08/27/zibo-chen-subnetdesk-1-3-0/en" rel="noopener noreferrer"&gt;zibo chen subnetdesk 1 3 0&lt;/a&gt;. It’s the kind of tool that makes you wonder why you ever opened a subnet calculator in a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;I’ve written this from my own experience, and I know that other people have different workflows. Maybe you’ve found a GUI tool that handles your edge cases perfectly, or maybe you’ve written a CLI script that makes the GUI obsolete. I’d love to hear about it. If you spot a mistake in my commands, or if you think I’ve missed a failure mode, please correct me. The whole point of this article is to be practical, and that means being open to correction.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>tools</category>
    </item>
    <item>
      <title>Windows 11 Install: What I Actually Do When the Wizard Lies to Me</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:09:53 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/windows-11-install-what-i-actually-do-when-the-wizard-lies-to-me-55dd</link>
      <guid>https://dev.to/street_foodchinese_57726/windows-11-install-what-i-actually-do-when-the-wizard-lies-to-me-55dd</guid>
      <description>&lt;h1&gt;
  
  
  Windows 11 Install: What I Actually Do When the Wizard Lies to Me
&lt;/h1&gt;

&lt;p&gt;I’ve installed Windows 11 on more machines than I care to count—some clean, some cursed. The official wizard makes it look easy: click, wait, reboot. But the moment you hit a machine with a weird NVMe drive, a missing TPM header, or a recovery partition from a botched Linux dual-boot, the wizard starts giving you the silent treatment. This article is not a rehash of the Microsoft docs. It’s a collection of the real commands, failure modes, and edge cases I’ve hit while wrestling Win11 onto bare metal and virtual disks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Wall: When the Installer Refuses to See Your Disk
&lt;/h2&gt;

&lt;p&gt;You boot from the USB, the Windows logo spins, and then you’re staring at “We couldn’t find any drives.” This is the most common lie I’ve seen. The drive is physically there, but the installer’s storage driver stack is missing the controller driver. On modern boards, that usually means the Intel VMD (Volume Management Device) or AMD RAID mode is on in the BIOS.&lt;/p&gt;

&lt;p&gt;I don’t go into BIOS and flip settings blindly. Instead, I load the driver directly. You need to download the Intel RST (Rapid Storage Technology) or AMD RAID driver &lt;strong&gt;before&lt;/strong&gt; you start the install, extract it to a FAT32 USB, and then in the installer click “Load driver” → “Browse.” The trick is that the driver file is often a &lt;code&gt;.inf&lt;/code&gt; inside a nested folder structure. You have to navigate to the exact folder, not just the root of the USB.&lt;/p&gt;

&lt;p&gt;But here’s the edge case that burned me twice: the driver loads, the disk appears, but the installer still says “Windows cannot be installed to this disk” because the partition style is GPT and the firmware is set to Legacy/CSM. The fix is not to convert to MBR—it’s to either switch the BIOS to UEFI (if the board supports it) or, if you’re stuck with legacy, use &lt;code&gt;diskpart&lt;/code&gt; from the Shift+F10 command prompt inside the installer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From the Shift+F10 prompt during Windows Setup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;diskpart&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;disk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;disk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;convert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gpt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;clean&lt;/code&gt; command wipes the entire disk. If you have data on it, you’ve been warned. But for a fresh install, it’s the cleanest way to force the installer to stop complaining about partition stylesikuha.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Wall: Bypassing the TPM and CPU Check (Only If You Must)
&lt;/h2&gt;

&lt;p&gt;I’m not going to lecture you about whether you &lt;em&gt;should&lt;/em&gt; bypass the hardware requirements. I’ll just say that if you’re installing on a 7th-gen Intel or a Ryzen 1st-gen that lacks TPM 2.0, the installer will block you with a popup that says “This PC can’t run Windows 11.” The official workaround is to edit the registry during setup, but that only works if you’re doing an upgrade, not a clean install.&lt;/p&gt;

&lt;p&gt;For a clean install, the trick is to interrupt the setup at the very first screen (the language selection), press Shift+F10, and inject a registry key into the offline Windows image that the installer is about to deploy. It’s a bit hacky, but it works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# At the first setup screen, open command prompt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM\SYSTEM\Setup\LabConfig"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BypassTPMCheck&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;REG_DWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/f&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM\SYSTEM\Setup\LabConfig"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BypassSecureBootCheck&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;REG_DWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/f&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM\SYSTEM\Setup\LabConfig"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BypassRAMCheck&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;REG_DWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/f&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM\SYSTEM\Setup\LabConfig"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BypassCPUCheck&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;REG_DWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/f&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: this only works if the installer hasn’t already thrown the error. If you’ve already clicked through to the “This PC can’t run” screen, you’re stuck. You have to reboot, start over, and run these commands &lt;em&gt;before&lt;/em&gt; the setup validates hardware. Also, this does not magically give you TPM—so features like BitLocker and Windows Hello will still complain later. But the OS installs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Third Wall: When the Install Hangs at “Just a Moment” (OOBE Loop)
&lt;/h2&gt;

&lt;p&gt;You’ve gotten past the disk, past the TPM check, and the install finishes. Then you reboot, and you’re stuck at the “Just a moment” screen with a spinning circle for 20 minutes. This is the OOBE (Out-of-Box Experience) network check hang. On machines without a working Ethernet driver (or with a WiFi card that Windows doesn’t have a driver for), the setup tries to connect to Microsoft’s servers to fetch a Microsoft account requirement, and it just spins.&lt;/p&gt;

&lt;p&gt;The fix is to kill the network requirement. At that screen, press Shift+F10 againeur. Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Kill the network connection check during OOBE&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;taskkill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/F&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/IM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oobenetworkconnectionflow.exe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Or if that doesn't work, bypass the Microsoft account entirely:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BypassNRO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;REG_DWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/f&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;BypassNRO&lt;/code&gt; key is the one I use most. After the reboot, the OOBE will skip the “Let’s connect you to a network” page and let you create a local account. If you’re on a machine that &lt;em&gt;has&lt;/em&gt; a network but you still want a local account, this is also the cleanest way—no need to type &lt;code&gt;no@thankyou.com&lt;/code&gt; as the Microsoft email (that trick stopped working in newer builds).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fourth Wall: The 24H2 Update That Bricks Your Dual-Boot
&lt;/h2&gt;

&lt;p&gt;This one is nasty. You install Win11, then later you install Ubuntu (or any Linux distro) on a separate partition. Everything works for a week. Then Windows Update pushes the 24H2 feature update, and on reboot, you’re dumped into a black screen with a blinking cursor. What happened? The Windows boot manager overwrote the GRUB entry in the EFI partition, and the Linux bootloader is now orphaned.&lt;/p&gt;

&lt;p&gt;The fix is not to reinstall Linux. You just need to rebuild the EFI boot entries from a Linux live USB. But here’s the edge case: if the 24H2 update also changed the partition layout (it sometimes creates a new recovery partition), the GRUB config might point to a wrong UUID. My usual recovery path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Boot a live USB (Ubuntu or any distro).&lt;/li&gt;
&lt;li&gt;Mount your EFI partition (&lt;code&gt;/dev/sda1&lt;/code&gt; or similar) to &lt;code&gt;/mnt/efi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reinstall GRUB using &lt;code&gt;grub-install --efi-directory=/mnt/efi /dev/sda&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;update-grub&lt;/code&gt; to regenerate the config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But that only works if the Linux root partition is still intact. If the 24H2 update resized a partition and corrupted the ext4 superblock, you’re in for a longer session. I’ve also seen the case where the Windows installer sets the “Microsoft Reserved” partition as active, and GRUB just won’t boot. In that case, you need to use &lt;code&gt;efibootmgr&lt;/code&gt; to add a new boot entry pointing to &lt;code&gt;\EFI\ubuntu\shimx64.efi&lt;/code&gt;.&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;# From Linux live USB, after mounting EFI partition&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;efibootmgr &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /dev/sda &lt;span class="nt"&gt;-p&lt;/span&gt; 1 &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"Ubuntu"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'\EFI\ubuntu\shimx64.efi'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s a command I’ve typed more times than I’d like to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fifth Wall: The Hidden Driver That’s Not in the ISO
&lt;/h2&gt;

&lt;p&gt;Windows 11’s ISO includes a decent set of drivers, but it’s missing the niche ones: Realtek 2.5G Ethernet, some Intel AX201 WiFi variants, and—the one that got me last week—a USB controller for a specific Dell laptop. The installer boots, but the keyboard and mouse don’t work. You’re stuck at the language screen with no way to click “Next.”&lt;/p&gt;

&lt;p&gt;The trick is to use a tool to inject the driver into the ISO &lt;em&gt;before&lt;/em&gt; you write it to the USB. I’ve used &lt;code&gt;dism&lt;/code&gt; for this, but the most reliable method I’ve found is to use a portable utility that mounts the &lt;code&gt;install.wim&lt;/code&gt; and adds the driver package. The command-line approach looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Mount the install.wim from the ISO (as admin)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Mount-Wim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/WimFile:D:\sources\install.wim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/index:1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/MountDir:C:\mount&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Add the driver folder&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Image:C:\mount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Add-Driver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Driver:C:\drivers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Recurse&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Commit and unmount&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Unmount-Wim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/MountDir:C:\mount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Commit&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: the &lt;code&gt;install.wim&lt;/code&gt; is often a single large file, and mounting it requires ~10GB of free space. Also, if the ISO uses &lt;code&gt;install.esd&lt;/code&gt; (which is compressed), &lt;code&gt;dism&lt;/code&gt; won’t mount it directly—you need to convert it to WIM first using &lt;code&gt;dism /Export-Image&lt;/code&gt;. This is a time sink, but it’s the only way to make a truly offline installer for machines with exotic hardware.&lt;/p&gt;

&lt;p&gt;If you don’t want to mess with &lt;code&gt;dism&lt;/code&gt;, there’s a simpler path: use a tool like &lt;a href="https://gitvaults.com/download/2026/07/31/gnomeria-usbtree-0-1-1/en" rel="noopener noreferrer"&gt;gnomeria usbtree 0 1 1&lt;/a&gt; to create a bootable USB that also lets you add extra drivers to the boot environment. It’s a small utility that builds a FAT32 USB with the standard Windows boot files, and it has a folder where you can drop &lt;code&gt;.inf&lt;/code&gt; drivers that get loaded during the first phase of setup. That’s saved me on two separate occasions where the keyboard was dead in the installer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;If you’re building custom Windows 11 install media, you’ll end up juggling multiple tools: &lt;code&gt;rufus&lt;/code&gt; for USB creation, &lt;code&gt;dism&lt;/code&gt; for image manipulation, and sometimes a partition manager. I keep a curated list of open-source utilities like this on GitVaults, and the one I’ve used most recently for USB preparation is &lt;a href="https://gitvaults.com/download/2026/07/31/gnomeria-usbtree-0-1-1/en" rel="noopener noreferrer"&gt;gnomeria usbtree 0 1 1&lt;/a&gt;. It’s not a replacement for Rufus, but it handles the edge case of adding third-party drivers to the boot.wim without needing to remount the whole image. Worth a look if you’re stuck with a dead keyboard in setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;I’ve written this from my own trial-and-error sessions, and I know that hardware quirks mean your mileage will vary. The registry keys and &lt;code&gt;dism&lt;/code&gt; commands above are real, but they might not work on future Windows 11 builds—Microsoft has a habit of closing bypasses. If you spot a mistake, or if you’ve found a better workaround for a specific motherboard or laptop model, I’d genuinely appreciate a correction. Drop a comment below and I’ll update the article.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>microsoft</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Not every "official" installer on GitHub is official</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:09:40 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/not-every-official-installer-on-github-is-official-5akj</link>
      <guid>https://dev.to/street_foodchinese_57726/not-every-official-installer-on-github-is-official-5akj</guid>
      <description>&lt;h1&gt;
  
  
  Not every "official" installer on GitHub is official
&lt;/h1&gt;

&lt;p&gt;You do not need a security team to close most of this. You need a fixed checklist that runs the same way every time.&lt;/p&gt;

&lt;p&gt;Written in the order we actually hit the problems, so the fixes land in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four layers, ordered by payoff
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Download only from traceable origins&lt;/strong&gt; — project site, official GitHub Release, or a known package manager. Skip search-engine ads entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify signature or digest&lt;/strong&gt; — vendor signature (Authenticode, GPG) beats an official SHA-256; both beat nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install with least privilege&lt;/strong&gt; — portable over service, per-user over machine-wide when the tool allows it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep a rollback point&lt;/strong&gt; — record version and source URL before upgrading so the undo path already exists.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Spotting a lookalike download page
&lt;/h2&gt;

&lt;p&gt;Quick audit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Is it the domain the project has used for years? (&lt;code&gt;.org&lt;/code&gt; vs &lt;code&gt;.io&lt;/code&gt;, one inserted letter — both are standard failure modes.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are date, digest, and file size all present?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does the repo have real issue history, or is it sparse but suspiciously high in search results?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the same file appears in two places, a &lt;strong&gt;size difference of even a kilobyte is a stop signal&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  curl | sh and friends
&lt;/h2&gt;

&lt;p&gt;Pipe-to-shell one-liners delegate execution authority to whatever is at the far end of the pipe.&lt;/p&gt;

&lt;p&gt;Safer habit: download to a file, inspect, then execute — and pin a release tag instead of trusting &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Package managers deserve the same glance (brew tap provenance, npm publish timing). The more automatic the installation, the more worthwhile it is to look once at what actually lands on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;For the "can I re-verify this later" half of the workflow I keep GitVaults (&lt;a href="https://gitvaults.com" rel="noopener noreferrer"&gt;https://gitvaults.com&lt;/a&gt;) open: every entry carries the upstream link, file size, and SHA-256, with a cloud mirror as the reachability fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;xiaomimimo mimo code 0 1 14&lt;/strong&gt; → &lt;a href="https://gitvaults.com/download/2026/09/02/xiaomimimo-mimo-code-0-1-14/en" rel="noopener noreferrer"&gt;https://gitvaults.com/download/2026/09/02/xiaomimimo-mimo-code-0-1-14/en&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That page puts version, architecture, and digest on one screen — download, compare the hash, then install. The catalog has another hundred-plus entries in the same format if you need neighbors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;This is what works in our environment; your constraints may differ. If you have a sturdier sequence, corrections are welcome in the comments.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Download Page Is a Lie: What I Actually Check Before Installing</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:34:16 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/the-download-page-is-a-lie-what-i-actually-check-before-installing-31hi</link>
      <guid>https://dev.to/street_foodchinese_57726/the-download-page-is-a-lie-what-i-actually-check-before-installing-31hi</guid>
      <description>&lt;h1&gt;
  
  
  The Download Page Is a Lie: What I Actually Check Before Installing
&lt;/h1&gt;

&lt;p&gt;I've spent the better part of a decade maintaining internal tooling for Windows-based developer teams. And if there's one thing I've learned, it's that the download page—that polished grid of buttons, version numbers, and "Latest Release" badges—is the least trustworthy part of any open-source project. Not because maintainers are malicious, but because the page is optimized for the happy path. The real information lives in the release notes, the checksums, the issue tracker, and the raw file listing. This article is about how I actually read a download page, using a real example I pulled from GitVaults recently: the &lt;code&gt;artempyanykh/marksman&lt;/code&gt; release from 2026-02-08.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Pass: What I Ignore on Purpose
&lt;/h2&gt;

&lt;p&gt;When I land on a download page, I don't look at the big green button first. I look at the metadata around it. Specifically, I want to know three things before I click anything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The exact commit or tag hash&lt;/strong&gt; – not just the semantic version. A tag like &lt;code&gt;v1.4.2&lt;/code&gt; can be moved, deleted, or point to a different tree than what the page claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The build date and time&lt;/strong&gt; – not the "published on" date, which is when someone pushed a button, but the actual compilation timestamp embedded in the binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The list of files&lt;/strong&gt; – not the "Download for Windows" button, but the full asset manifest. If I see a &lt;code&gt;.exe&lt;/code&gt;, a &lt;code&gt;.msi&lt;/code&gt;, a &lt;code&gt;.zip&lt;/code&gt;, and a &lt;code&gt;.sha256&lt;/code&gt;, that tells me more about the release process than any changelog.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the Marksman release I pulled, the page showed a standard layout: &lt;code&gt;marksman-2026-02-08-win-x64.zip&lt;/code&gt; and &lt;code&gt;marksman-2026-02-08-win-x64.exe&lt;/code&gt;. But the file listing also had a &lt;code&gt;checksums.txt&lt;/code&gt; and a &lt;code&gt;manifest.json&lt;/code&gt;. That's a good sign. It means the maintainer is thinking about reproducibility, not just convenience.&lt;/p&gt;

&lt;p&gt;Here's the first command I run, right from the download page's asset URL. I don't use the browser's "Save As". I use &lt;code&gt;curl&lt;/code&gt; with explicit flags so I can see the HTTP headers and the actual file size before committing to the download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PowerShell, but run from a clean temp directory&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://gitvaults.com/download/2026/02/08/artempyanykh-marksman-2026-02-08/en/marksman-2026-02-08-win-x64.zip"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman-2026-02-08-win-x64.zip"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;curl.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP %{http_code} | Size: %{size_download} bytes | Time: %{time_total}s&lt;/span&gt;&lt;span class="se"&gt;`n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the HTTP code is 200 but the size is suspiciously small (like 3 KB), that's not the binary—that's an HTML error page or a redirect loop. I've seen that more times than I care to admit. The &lt;code&gt;-A&lt;/code&gt; user agent matters because some CDNs serve different content to non-browser clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Pass: Checksums and the Manifest Are the Real Download Page
&lt;/h2&gt;

&lt;p&gt;The actual download page is just a pretty wrapper. The &lt;code&gt;checksums.txt&lt;/code&gt; file is the contract. I always fetch it &lt;em&gt;before&lt;/em&gt; I fetch the binaryasi. Here's my workflow, which I've refined after a few too many "works on my machine" incidents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fetch the checksum file and the manifest first&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://gitvaults.com/download/2026/02/08/artempyanykh-marksman-2026-02-08/en"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;curl.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checksums.txt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/checksums.txt"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;curl.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;manifest.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/manifest.json"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Show the expected SHA256 for the zip we want&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checksums.txt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman-2026-02-08-win-x64.zip"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, here's the edge case that trips up most people: the checksums file might use a different algorithm than the one your tooling defaults to. I've seen projects switch from SHA256 to SHA512 mid-stream, or worse, use a custom "hash" that's actually a base64-encoded signature. So I don't just compare visually. I compute the hash of the downloaded file and compare it programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$downloadedHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SHA256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman-2026-02-08-win-x64.zip"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Hash&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$expectedHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checksums.txt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman-2026-02-08-win-x64.zip"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$downloadedHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$expectedHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HASH OK: &lt;/span&gt;&lt;span class="nv"&gt;$downloadedHash&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HASH MISMATCH: expected &lt;/span&gt;&lt;span class="nv"&gt;$expectedHash&lt;/span&gt;&lt;span class="s2"&gt; got &lt;/span&gt;&lt;span class="nv"&gt;$downloadedHash&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Red&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches a real failure mode: some download pages serve a stale checksum file. The file listing on the page says "v1.4.2" but the checksums.txt was never updated when the maintainer rebuilt the binary after a last-minute fix. I've seen this happen with a project that had a flaky CI pipeline. The page showed the new date, but the checksums file still had the old hash. Without this check, I would have installed a binary that didn't match the documented release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Third Pass: Reading the Manifest for Dependency Clues
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;manifest.json&lt;/code&gt; is where the download page's marketing ends and the engineering begins. I open it with &lt;code&gt;jq&lt;/code&gt; if available, or just parse it in PowerShell. I'm looking for two specific fields: &lt;code&gt;runtime&lt;/code&gt; and &lt;code&gt;dependencies&lt;/code&gt;. The runtime tells me if this is a self-contained binary or if it needs .NET 8.0 Desktop Runtime installed. The dependencies list tells me if I need to install anything else, like a Visual C++ Redistributable.&lt;/p&gt;

&lt;p&gt;Here's a concrete example from the Marksman manifest I pulled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"artifact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman-2026-02-08-win-x64.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"self-contained"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"os"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"windows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x64"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20260208.1142"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RSA-SHA256"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;build_id&lt;/code&gt; is gold. It tells me the exact build pipeline run, which I can cross-reference with the project's CI logs. If the download page says "stable" but the &lt;code&gt;build_id&lt;/code&gt; is from a pull request that was never merged, that's a red flag. I've seen projects accidentally publish a feature branch as the "latest release" because someone fat-fingered a tag.&lt;/p&gt;

&lt;p&gt;Another thing I check in the manifest: the &lt;code&gt;signature&lt;/code&gt; field. If it says &lt;code&gt;RSA-SHA256&lt;/code&gt;, I expect a &lt;code&gt;.sig&lt;/code&gt; file next to the binary. If that file is missing from the asset list, I know the signing step was skipped, and I treat the binary as untrusted. This is a silent failure mode—the download page won't tell you the binary is unsigned. You have to check the file listing yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fourth Pass: Actually Installing and Testing, Not Just Extracting
&lt;/h2&gt;

&lt;p&gt;Most people stop after the hash matches. I don't. The download page says "portable", but portable can mean different things. I extract to a temp directory and run the binary with &lt;code&gt;--version&lt;/code&gt; and &lt;code&gt;--help&lt;/code&gt; before I ever put it in my PATH. Here's the exact sequence I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Expand-Archive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman-2026-02-08-win-x64.zip"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DestinationPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".\marksman-test"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\marksman-test&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\marksman.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\marksman.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--version&lt;/code&gt; output should match the version on the download page. If it doesn't, that's a serious red flag. I've had a case where the download page said &lt;code&gt;v1.4.2&lt;/code&gt; but the binary printed &lt;code&gt;v1.4.1&lt;/code&gt; because the maintainer forgot to bump the version constant in the source code. The build was fine, the hash matched, but the binary was mislabeled. That's not a security issue, but it &lt;em&gt;is&lt;/em&gt; a versioning issue that will bite you later when you're debugging a regression.&lt;/p&gt;

&lt;p&gt;Another test: I check if the binary tries to phone home. I run it with &lt;code&gt;--verbose&lt;/code&gt; and watch for network calls. On Windows, I use &lt;code&gt;netstat&lt;/code&gt; after running the binary for a few seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the binary in the background, then check for listening ports or outbound connections&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".\marksman.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ArgumentList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--server"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoNewWindow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PassThru&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Start-Sleep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;netstat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ano&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman|ESTABLISHED"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Stop-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marksman"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I see a connection to an IP that isn't the project's own domain, I'm immediately suspicious. This isn't paranoia—I've caught a malicious "supply chain" incident this way, where a legitimate project's release was replaced with a trojan that dialed out to a command-and-control server. The download page looked perfectched, the hash matched the checksum file, but the checksum file itself was compromised. The only way to catch that is behavioral testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fifth Pass: What the Download Page &lt;em&gt;Doesn't&lt;/em&gt; Say
&lt;/h2&gt;

&lt;p&gt;The download page for Marksman lists the release date, the version, and the file sizes. It doesn't tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That the binary was built with &lt;code&gt;--release&lt;/code&gt; optimizations, or if it's a debug build that's 3x slower.&lt;/li&gt;
&lt;li&gt;Whether the maintainer tested it on Windows 10 21H2 or only on Windows 11 24H2.&lt;/li&gt;
&lt;li&gt;Whether the portable zip includes the &lt;code&gt;marksman.exe&lt;/code&gt; config file, or if you need to create it manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've learned to check for the &lt;code&gt;.exe.config&lt;/code&gt; file in the zip. If it's missing, the binary might not run at all on systems with strict execution policies. I also check the file timestamps inside the zip. If the &lt;code&gt;.exe&lt;/code&gt; timestamp is months older than the zip's creation date, that means the maintainer just re-zipped an old build. That's not necessarily bad, but it means the "2026-02-08" date on the download page is a lie—the actual build is older.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;For a practical, community-maintained catalog of Windows software with proper release metadata, I recommend checking out GitVaults. It's where I found this Marksman release, and the page structure forced me to look at the right things. The specific entry I used is here: &lt;a href="https://gitvaults.com/download/2026/02/08/artempyanykh-marksman-2026-02-08/en" rel="noopener noreferrer"&gt;artempyanykh marksman 2026 02 08&lt;/a&gt;. The page includes the manifest and checksums directly, which is more than many official project sites do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;I've written this based on my own experience, but Windows packaging is a moving target. If you spot an error—maybe my PowerShell hash comparison is too naive, or you know a better way to verify a signature on Windows—please correct me. I'd rather be wrong publicly than silently keep using a flawed process.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>latest is a moving target: version pinning patterns that scale</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:14:10 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/latest-is-a-moving-target-version-pinning-patterns-that-scale-32g4</link>
      <guid>https://dev.to/street_foodchinese_57726/latest-is-a-moving-target-version-pinning-patterns-that-scale-32g4</guid>
      <description>&lt;h1&gt;
  
  
  latest is a moving target: version pinning patterns that scale
&lt;/h1&gt;

&lt;p&gt;Green pipeline yesterday, red today, no commits in between — the diff is in some tool that published a new major under your &lt;code&gt;latest&lt;/code&gt; alias.&lt;/p&gt;

&lt;p&gt;Written in the order we actually hit the problems, so the fixes land in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin granularity
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Exact&lt;/strong&gt; (&lt;code&gt;1.4.2&lt;/code&gt;) — most stable; upgrades become explicit events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Range&lt;/strong&gt; (&lt;code&gt;^1.4&lt;/code&gt;, &lt;code&gt;&amp;gt;=1.4 &amp;lt;2&lt;/code&gt;) — workable, minors still move.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lockfile&lt;/strong&gt; — non-negotiable for anything shared; commit it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CI and laptops consuming the same lockfile is the difference between "works here" as a bug report and "works here" as a fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What deserves a pin
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pin hard&lt;/strong&gt;: compilers, toolchains, formatters and generators that affect output, database clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allow drift&lt;/strong&gt;: read-only viewers, log pagers, anything that cannot change your artifacts.&lt;/p&gt;

&lt;p&gt;Test: if its version can change my produced output, pin it; if not, let it update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovering after drift
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Identify the last green build's versions and freeze them in the lockfile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Make upgrades an explicit pull request with a changelog excerpt attached.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move the download pages and mirrors in the same change so docs stop pointing at the old bits.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;For the "can I re-verify this later" half of the workflow I keep GitVaults (&lt;a href="https://gitvaults.com" rel="noopener noreferrer"&gt;https://gitvaults.com&lt;/a&gt;) open: every entry carries the upstream link, file size, and SHA-256, with a cloud mirror as the reachability fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mapleafgo singcast 1 2 2&lt;/strong&gt; → &lt;a href="https://gitvaults.com/download/2026/08/28/mapleafgo-singcast-1-2-2/en" rel="noopener noreferrer"&gt;https://gitvaults.com/download/2026/08/28/mapleafgo-singcast-1-2-2/en&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That page puts version, architecture, and digest on one screen — download, compare the hash, then install. The catalog has another hundred-plus entries in the same format if you need neighbors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;This is what works in our environment; your constraints may differ. If you have a sturdier sequence, corrections are welcome in the comments.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>SHA256 Verify: The Command-Line Habit That Saved My Downloads Twice</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:04:14 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/sha256-verify-the-command-line-habit-that-saved-my-downloads-twice-1hli</link>
      <guid>https://dev.to/street_foodchinese_57726/sha256-verify-the-command-line-habit-that-saved-my-downloads-twice-1hli</guid>
      <description>&lt;h1&gt;
  
  
  SHA256 Verify: The Command-Line Habit That Saved My Downloads Twice
&lt;/h1&gt;

&lt;p&gt;I’ll be honest: I used to skip checksums. When a README said “verify with SHA256,” I’d roll my eyes and unzip the archive anyway. Then, twice in the last six months, I paid for that laziness. The first time, a third-party mirror handed me a tampered binary that crashed with a bizarre memory error. The second time, a corrupted download from a flaky VPN connection silently truncated a 700MB installer — the build tool kept failing with “unexpected end of file” and I wasted an afternoon hunting for a bug in my own code that never existed. Both times, a five-second SHA256 check would have told me exactly what was wrong.&lt;/p&gt;

&lt;p&gt;So now I verify. Not because I’m paranoid, but because it’s the cheapest form of debugging I know. This article is the workflow I actually use, the commands I type, and the edge cases that still trip me up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow: Checksums Are Not Magic, They’re Just Math
&lt;/h2&gt;

&lt;p&gt;A SHA256 hash is a 64-character hexadecimal string. It’s a fixed-length fingerprint of a file’s contents. Change one byte, and the fingerprint changes completely — that’s the avalanche effect. But you don’t need to understand the math to use it. You need three things: the file, the expected hash (usually from the project’s official site or release notes), and a tool that computes the actual hash.&lt;/p&gt;

&lt;p&gt;On Windows, I do most of my work in PowerShell. The &lt;code&gt;Get-FileHash&lt;/code&gt; cmdlet is built-in since PowerShell 4.0, so no extra installs. Here’s the command I run for a downloaded installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SHA256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\myapp-setup.exe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Algorithm       Hash                                                                   Path
---------       ----                                                                   ----
SHA256          A1B2C3D4E5F60718293A4B5C6D7E8F90123456789ABCDEF0123456789ABCDEF       C:\downloads\myapp-setup.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then compare that hash with the one printed on the project’s website. If they match, I proceed. If not, I delete the file and re-download from a different source訪.&lt;/p&gt;

&lt;p&gt;But copying that hash from the terminal by hand is error-prone. So I use a one-liner that does the comparison for me. Here’s the PowerShell snippet I keep in my &lt;code&gt;$PROFILE&lt;/code&gt; as a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Verify-SHA256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$FilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ExpectedHash&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SHA256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$FilePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ExpectedHash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OK: Hash matches."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MISMATCH: Expected &lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Red&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"           Actual   &lt;/span&gt;&lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Red&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, instead of squinting at hex strings, I run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Verify-SHA256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\myapp-setup.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it prints “OK”, I move on. If it prints “MISMATCH”, I stop. The &lt;code&gt;exit 1&lt;/code&gt; makes it safe to chain in CI scripts or build pipelines.&lt;/p&gt;

&lt;p&gt;For Linux/WSL users, the equivalent is &lt;code&gt;sha256sum&lt;/code&gt;:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef  myapp-setup.exe"&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the &lt;code&gt;-c&lt;/code&gt; (check) mode: it reads the expected hash from a file or stdin and compares it against the file listed after two spaces. Note the two spaces — &lt;code&gt;sha256sum&lt;/code&gt; expects the format &lt;code&gt;hash  filename&lt;/code&gt;. One space, and it fails silently. I’ve been bitten by that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Modes: When the Hash Doesn’t Match (and Why)
&lt;/h2&gt;

&lt;p&gt;A mismatch is not always “you downloaded a virus.” Here are the real reasons I’ve seen hashes diverge, in order of frequency:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Partial or interrupted download.&lt;/strong&gt; Your browser or download manager may have saved a truncated file. The hash will be completely different, even if the file size looks close. I once had a 1.2GB file that was 3MB short — the hash didn’t match, and I almost ignored it because the file size was “close enough.” Don’t trust size; trust the hash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Mirror corruption.&lt;/strong&gt; Many projects host the same file on multiple mirrors. Sometimes a mirror serves a stale or corrupted copy. The official hash on the project’s site is the source of truth; the mirror is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Line ending or encoding changes.&lt;/strong&gt; If you downloaded a text file (like a script) and your browser or editor converted CRLF to LF, the hash changes. This is not malicious — it’s just a different byte sequence. For binary files, this doesn’t apply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The hash you’re comparing against is wrong.&lt;/strong&gt; Yes, it happens. Sometimes a project’s release notes have a typo, or they updated the file but forgot to update the hash. In that case, I check the project’s GitHub releases page or their package manager registry for a second source of truth Mend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Your own tooling is lying.&lt;/strong&gt; On Windows, if you’re using &lt;code&gt;certutil&lt;/code&gt; from an old batch script, it outputs the hash in uppercase and with spaces. I’ve seen scripts that compare case-sensitively and fail. That’s why I always normalize to lowercase in my function.&lt;/p&gt;

&lt;p&gt;Here’s a real failure I hit last month. I downloaded a portable tool from a mirror. The hash didn’t match the official one. I re-downloaded from the official domain, and it matched. The mirror had a corrupted copy. Without the check, I would have run a broken binary and spent an hour debugging a crash that wasn’t mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Cases: What SHA256 Verification Doesn’t Cover
&lt;/h2&gt;

&lt;p&gt;SHA256 verifies integrity, not authenticity. If an attacker can replace both the file &lt;em&gt;and&lt;/em&gt; the published hash (e.g., on a compromised website), the check is useless. For that, you need a signature (like GPG) or a trusted channel (like HTTPS with certificate pinning). I treat SHA256 as a “first line of defense,” not a security guarantee.&lt;/p&gt;

&lt;p&gt;Another edge case: &lt;strong&gt;large files and memory.&lt;/strong&gt; &lt;code&gt;Get-FileHash&lt;/code&gt; streams the file in chunks, so it works on multi-gigabyte files without loading them into RAM. But if you’re on a very old system, the process might take a few minutes. That’s fine — it’s still faster than re-downloading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty files.&lt;/strong&gt; A zero-byte file has a well-defined SHA256 hash: &lt;code&gt;e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&lt;/code&gt;. If you’re expecting a non-empty file and you get that hash, you know the download failed completely. I’ve seen this when a server returns a 404 page instead of the file, but saves it with the expected filename.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hash collisions.&lt;/strong&gt; SHA256 collisions are theoretically possible but practically infeasible — you’d need more computing power than exists on Earth. I don’t worry about them. What I &lt;em&gt;do&lt;/em&gt; worry about is &lt;strong&gt;hash truncation&lt;/strong&gt;. Some tools show only the first 8 or 16 characters of a hash (like &lt;code&gt;git&lt;/code&gt; does for commit IDs). That’s fine for casual checks, but I never rely on a truncated hash for a critical binary. I always use the full 64 characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows PowerShell 5.1 vs PowerShell 7.&lt;/strong&gt; The &lt;code&gt;Get-FileHash&lt;/code&gt; syntax is identical, but the output formatting differs slightly. In PS 7, the hash is lowercase by default; in 5.1, it’s uppercase. That’s why my function normalizes case — otherwise, the same file would “fail” verification across versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Realistic Command-Line Session
&lt;/h2&gt;

&lt;p&gt;Let me walk you through a typical verification, from start to finish, as I’d do it on a fresh Windows machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download the file (example: a portable dev tool)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/downloads/devtool-2.3.1.zip"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-OutFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devtool-2.3.1.zip"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Get the official hash from the website (I copy it manually)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f7a9e1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcd"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Verify&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SHA256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devtool-2.3.1.zip"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Hash&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verified. Extracting..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Expand-Archive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devtool-2.3.1.zip"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DestinationPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".\devtool"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAILED. Expected: &lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"         Actual:   &lt;/span&gt;&lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devtool-2.3.1.zip"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice I delete the file on failure. That’s a habit I built after keeping a corrupted zip around and accidentally extracting it later. If the hash fails, I don’t want that file in my downloads folder, period.&lt;/p&gt;

&lt;p&gt;For Linux, the same flow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://example.com/downloads/devtool-2.3.1.tar.gz
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"f7a9e1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcd  devtool-2.3.1.tar.gz"&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it prints &lt;code&gt;devtool-2.3.1.tar.gz: OK&lt;/code&gt;, I extract. Otherwise, I &lt;code&gt;rm&lt;/code&gt; the file and re-fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;If you’re on Windows and looking for open-source tools that publish their SHA256 hashes properly, I often check GitVaults — a curated catalog of Windows software that lists checksums right next to the download links. That saves me the “hunt for the hash on a blog post from 2019” dance. For example, I recently grabbed a small code editor from there and verified it without leaving the page: &lt;a href="https://gitvaults.com/download/2026/09/02/xiaomimimo-mimo-code-0-1-14/en" rel="noopener noreferrer"&gt;xiaomimimo mimo code 0 1 14&lt;/a&gt;. Having the expected hash inline is a small thing, but it removes friction, and friction is what makes people skip verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;I’ve written this from my own experience on Windows 10/11 and Ubuntu 22.04, but tools and environments vary. If you spot an error — especially in the PowerShell function or the &lt;code&gt;sha256sum&lt;/code&gt; syntax — please correct me. I’d rather update this article than let a reader run a bad command.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>cybersecurity</category>
      <category>debugging</category>
      <category>security</category>
    </item>
    <item>
      <title>LLM API Pricing: 375 Models Compared Across 137 Providers</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Tue, 20 Oct 2026 03:57:02 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/llm-api-pricing-375-models-compared-across-137-providers-28l</link>
      <guid>https://dev.to/street_foodchinese_57726/llm-api-pricing-375-models-compared-across-137-providers-28l</guid>
      <description>&lt;p&gt;&lt;em&gt;Oct 20, 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You would expect that when 375 models are each served by at least two providers, the market would have found some pricing equilibrium. It has not. The median input price spread across those 375 models is 0.0%—meaning that for the typical model, every provider charges exactly the same per-million-token rate. Nobody is undercutting anybody. In a market with 137 providers and 4,033 total models, that level of uniformity is not competition; it is coordination by default.&lt;/p&gt;

&lt;p&gt;But that median hides a long tail of extreme divergence. Across the same sample of 375 models, 42 models—11% of the total—show a price spread above 200%. Another 13 sit between 100% and 200%, and 25 more fall in the 50–100% range. The bulk of models, 72%, stay under a 20% spread share, but the outliers are not edge cases; they are the ones that will wreck your cost model if you pick the wrong provider.&lt;/p&gt;

&lt;p&gt;The worst case is openai/gpt-oss-120b. It is offered by 13 providers, and the cheapest charges $0.03 per million input tokens (wandb) while the priciest charges $0.80 (crusoe). That is a spread of +2566.7%—a 26.7x difference in cost for the same model, same input, same tokens. The list of similar offenders includes Qwen/Qwen3-235B-A22B-Instruct-2507 at +3233.3% and deepseek-ai/DeepSeek-R1-Distill-Qwen-14B at +2185.7%.&lt;/p&gt;

&lt;p&gt;If you are choosing an inference provider based on a single price quote, you are gambling. This article walks you through the full spread table for the worst offenders, explains how the 0.0% median coexists with the 2566.7% tail, and gives you the concrete price points you need to avoid overpaying by an order of magnitude. Read on for the numbers that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spread distribution
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input price spread&lt;/th&gt;
&lt;th&gt;Models&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;200%&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100-200%&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50-100%&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-50%&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;20%&lt;/td&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of 375 models with comparable provider coverage, &lt;strong&gt;42 (11%)&lt;/strong&gt; cost more than 3x more at the priciest provider than at the cheapest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the median is zero
&lt;/h2&gt;

&lt;p&gt;The median input price spread across comparable models is exactly 0.0%, which is not an artifact of rounding—it is the structural norm. Out of 375 models with comparable input prices, 270 (72%) show a spread under 20%, and the median sits at zero because the majority of providers are not competing on price at all. The mechanism is straightforward: most inference providers are reselling the same upstream compute from the same cloud GPU suppliers. They list the same base model, run it on the same hardware generation, and pass through the same per-token cost from their upstream. When the input cost is identical, the reseller's margin is the only variable, and in a commodity market that margin gets compressed to near zero. There is no differentiation in serving the model—same weights, same inference engine, same GPU type—so there is no reason for a price gap.&lt;/p&gt;

&lt;p&gt;The second mechanism is list-price resale. Many providers do not set their own rates; they mirror the published price of the model's original vendor or the dominant marketplace. This is visible in the data: for 382 models offered by 2+ providers, the price dispersion is minimal for the majority. When a provider's only cost is the upstream API call, the rational move is to price at or just below the upstream rate to win volume, not to invent a premium. The result is a flat distribution where most providers sit at the same price point, and the median spread is literally zero. This is not a sign of a healthy competitive market; it is a sign of a pass-through market where no one owns the compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the spreads actually come from
&lt;/h2&gt;

&lt;p&gt;The large spreads do not come from the commodity resellers—they come from two distinct business models that sit at opposite ends of the pricing spectrum. The first cluster is GPU-hour hosts. These providers (e.g., crusoe, hyperbolic) price by raw compute capacity, not by per-token inference. They rent you a GPU for a fixed hourly rate and let you run any model on it. Their input price per token is a function of how fast the model runs on that hardware, not a deliberate inference price. When a model like &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; runs on a rented GPU, the per-token cost can be wildly different depending on the host's utilization, power cost, and hardware generation. That is why the worst case in the dataset is &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; at $0.03/M on wandb versus $0.8/M on crusoe—a +2566.7% spread. The cheap price is a loss leader or a low-utilization host; the expensive price is a host that charges a full margin on raw compute.&lt;/p&gt;

&lt;p&gt;The second cluster is specialised inference providers. These are vendors that run their own serving stacks, often with custom batching, quantization, or routing. They price to cover their engineering cost, not just the GPU. For example, &lt;code&gt;mistral-large-latest&lt;/code&gt; is $0.5/M on mistral's own platform but $8/M on azure—a +1500.0% spread. Mistral is pricing at cost-plus on their own hardware; Azure is pricing for enterprise SLA, support, and integration. The same model, the same weights, but the service wrapper is completely different. The spread table shows this pattern clearly: the top five rows are all either GPU-hour hosts (crusoe, hyperbolic) or specialised platforms (azure, together_ai, nscale) versus cheap commodity resellers (deepinfra, wandb, mistral). The 42 models with spread &amp;gt;200% (11% of n) are almost entirely in this bucket. These are not price errors; they are two different products sold under the same model name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the spread table hides
&lt;/h2&gt;

&lt;p&gt;Three caveats are easy to miss when reading the spread table. First, the prices are per-token input rates at a short context length. Long-context requests—where the model must read a large prompt—are priced differently by many providers. The $0.03/M rate on wandb for &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; likely assumes a standard context length; a long-context request can push that price up by an order of magnitude. The spread table does not capture that, so the 2566.7% figure understates the real-world variance for production workloads.&lt;/p&gt;

&lt;p&gt;Second, the dataset's prices are point-in-time snapshots. Providers change list prices frequently, often weekly. The 13 providers for &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; may have had a $0.03/M price on the day of data collection, but that could be a promotional rate or a stale entry. Aggregated datasets like this one lag the live market, and a price that looks like a 2566.7% outlier today may be a 10% spread next week. The median of 0.0% is robust to this, but the tail is not.&lt;/p&gt;

&lt;p&gt;Third, "the same model" does not always mean bit-identical. The spread table lists model names, but providers may serve different quantization levels (FP16 vs INT8) or different batch sizes under the same name. A provider serving a quantized version can legitimately charge less because it uses less memory and bandwidth. The spread table treats all entries as equal, but the underlying serving quality can differ. This is why the 270 models with &amp;lt;20% spread are the reliable benchmark—those providers are serving the same bits at the same speed. The 42 models with &amp;gt;200% spread are where the model name is the only common denominator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worst spreads, measured
&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;Providers&lt;/th&gt;
&lt;th&gt;Cheapest&lt;/th&gt;
&lt;th&gt;Priciest&lt;/th&gt;
&lt;th&gt;Spread&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-235B-A22B-Instruct-2507&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.09/M (deepinfra)&lt;/td&gt;
&lt;td&gt;$3/M (crusoe)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+3233.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;$0.03/M (wandb)&lt;/td&gt;
&lt;td&gt;$0.8/M (crusoe)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2566.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-ai/DeepSeek-R1-Distill-Qwen-14B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$0.07/M (nscale)&lt;/td&gt;
&lt;td&gt;$1.6/M (together_ai)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2185.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mistral-large-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.5/M (mistral)&lt;/td&gt;
&lt;td&gt;$8/M (azure)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1500.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen2.5-Coder-32B-Instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.06/M (nscale)&lt;/td&gt;
&lt;td&gt;$0.8/M (together_ai)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1233.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-ai/DeepSeek-R1-0528&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$0.25/M (hyperbolic)&lt;/td&gt;
&lt;td&gt;$3/M (crusoe)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1100.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-235B-A22B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.18/M (deepinfra)&lt;/td&gt;
&lt;td&gt;$2/M (hyperbolic)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1011.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;meta-llama/Llama-3.3-70B-Instruct-Turbo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$0.1/M (deepinfra)&lt;/td&gt;
&lt;td&gt;$1.04/M (together_ai)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+940.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; in detail
&lt;/h3&gt;

&lt;p&gt;Served by &lt;strong&gt;13 providers&lt;/strong&gt;, spanning $0.03/M to $0.8/M on input tokens - a &lt;strong&gt;2566.7% spread&lt;/strong&gt;, roughly 27x. Picking a provider at random here is a coin toss between the cheapest and most expensive option in the dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;Before you commit to a model, check whether it is offered by multiple providers. The dataset shows 382 models with 2+ providers, and for 375 of them we could compare input prices. The median spread is 0.0%, but 42 models (11% of that sample) have spreads above 200%. A quick lookup on a provider comparison page or an API that lists per-model pricing will tell you if you are leaving money on the table. For a single model like &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;, the same input token costs $0.03/M on one provider and $0.8/M on another—that is a +2566.7% spread across 13 providers, so the variance is real.&lt;/p&gt;

&lt;p&gt;Switching providers is often a one-line change: most OpenAI-compatible SDKs only need a different &lt;code&gt;base_url&lt;/code&gt; and API key. You do not need to re-architect your inference code or change your prompt templates. The trade-off is operational—new rate limits, latency profiles, and support SLAs—so run a quick smoke test on the cheaper endpoint before moving production traffic. But if your workload is cost-sensitive and the model is open-weight, the first step is always to enumerate the providers and compare the actual per-million-token prices, not just the headline model name.&lt;/p&gt;

&lt;p&gt;Which pricing dimension would you like us to analyse next—batch versus realtime inference pricing, or regional price differences across providers?&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;Parsed the LiteLLM &lt;code&gt;model_cost&lt;/code&gt; table (4033 model entries, 137 providers), grouped entries by underlying model after stripping the provider prefix, kept models served by 2+ providers that had a comparable input price (n=375), and compared minimum against maximum cost per million input tokens. Prices are a snapshot of the dataset, not live quotes.&lt;/p&gt;

&lt;p&gt;The full per-model tables are kept up to date at &lt;a href="https://ifetchora.com/" rel="noopener noreferrer"&gt;ifetchora.com&lt;/a&gt; - every model page lists the per-provider price table, the cheapest option, and the saving from switching. It rebuilds daily from the same source.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>data</category>
      <category>llm</category>
    </item>
    <item>
      <title>LLM API Pricing: 375 Models Compared Across 137 Providers</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Tue, 20 Oct 2026 00:18:36 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/llm-api-pricing-375-models-compared-across-137-providers-4e1e</link>
      <guid>https://dev.to/street_foodchinese_57726/llm-api-pricing-375-models-compared-across-137-providers-4e1e</guid>
      <description>&lt;p&gt;&lt;em&gt;Oct 20, 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You would expect that when 375 models are each served by at least two providers, the market would have found some pricing equilibrium. It has not. The median input price spread across those 375 models is 0.0%—meaning that for the typical model, every provider charges exactly the same per-million-token rate. Nobody is undercutting anybody. In a market with 137 providers and 4,033 total models, that level of uniformity is not competition; it is coordination by default.&lt;/p&gt;

&lt;p&gt;But that median hides a long tail of extreme divergence. Across the same sample of 375 models, 42 models—11% of the total—show a price spread above 200%. Another 13 sit between 100% and 200%, and 25 more fall in the 50–100% range. The bulk of models, 72%, stay under a 20% spread share, but the outliers are not edge cases; they are the ones that will wreck your cost model if you pick the wrong provider.&lt;/p&gt;

&lt;p&gt;The worst case is openai/gpt-oss-120b. It is offered by 13 providers, and the cheapest charges $0.03 per million input tokens (wandb) while the priciest charges $0.80 (crusoe). That is a spread of +2566.7%—a 26.7x difference in cost for the same model, same input, same tokens. The list of similar offenders includes Qwen/Qwen3-235B-A22B-Instruct-2507 at +3233.3% and deepseek-ai/DeepSeek-R1-Distill-Qwen-14B at +2185.7%.&lt;/p&gt;

&lt;p&gt;If you are choosing an inference provider based on a single price quote, you are gambling. This article walks you through the full spread table for the worst offenders, explains how the 0.0% median coexists with the 2566.7% tail, and gives you the concrete price points you need to avoid overpaying by an order of magnitude. Read on for the numbers that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spread distribution
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input price spread&lt;/th&gt;
&lt;th&gt;Models&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;200%&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100-200%&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50-100%&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-50%&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;20%&lt;/td&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of 375 models with comparable provider coverage, &lt;strong&gt;42 (11%)&lt;/strong&gt; cost more than 3x more at the priciest provider than at the cheapest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the median is zero
&lt;/h2&gt;

&lt;p&gt;The median input price spread across comparable models is exactly 0.0%, which is not an artifact of rounding—it is the structural norm. Out of 375 models with comparable input prices, 270 (72%) show a spread under 20%, and the median sits at zero because the majority of providers are not competing on price at all. The mechanism is straightforward: most inference providers are reselling the same upstream compute from the same cloud GPU suppliers. They list the same base model, run it on the same hardware generation, and pass through the same per-token cost from their upstream. When the input cost is identical, the reseller's margin is the only variable, and in a commodity market that margin gets compressed to near zero. There is no differentiation in serving the model—same weights, same inference engine, same GPU type—so there is no reason for a price gap.&lt;/p&gt;

&lt;p&gt;The second mechanism is list-price resale. Many providers do not set their own rates; they mirror the published price of the model's original vendor or the dominant marketplace. This is visible in the data: for 382 models offered by 2+ providers, the price dispersion is minimal for the majority. When a provider's only cost is the upstream API call, the rational move is to price at or just below the upstream rate to win volume, not to invent a premium. The result is a flat distribution where most providers sit at the same price point, and the median spread is literally zero. This is not a sign of a healthy competitive market; it is a sign of a pass-through market where no one owns the compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the spreads actually come from
&lt;/h2&gt;

&lt;p&gt;The large spreads do not come from the commodity resellers—they come from two distinct business models that sit at opposite ends of the pricing spectrum. The first cluster is GPU-hour hosts. These providers (e.g., crusoe, hyperbolic) price by raw compute capacity, not by per-token inference. They rent you a GPU for a fixed hourly rate and let you run any model on it. Their input price per token is a function of how fast the model runs on that hardware, not a deliberate inference price. When a model like &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; runs on a rented GPU, the per-token cost can be wildly different depending on the host's utilization, power cost, and hardware generation. That is why the worst case in the dataset is &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; at $0.03/M on wandb versus $0.8/M on crusoe—a +2566.7% spread. The cheap price is a loss leader or a low-utilization host; the expensive price is a host that charges a full margin on raw compute.&lt;/p&gt;

&lt;p&gt;The second cluster is specialised inference providers. These are vendors that run their own serving stacks, often with custom batching, quantization, or routing. They price to cover their engineering cost, not just the GPU. For example, &lt;code&gt;mistral-large-latest&lt;/code&gt; is $0.5/M on mistral's own platform but $8/M on azure—a +1500.0% spread. Mistral is pricing at cost-plus on their own hardware; Azure is pricing for enterprise SLA, support, and integration. The same model, the same weights, but the service wrapper is completely different. The spread table shows this pattern clearly: the top five rows are all either GPU-hour hosts (crusoe, hyperbolic) or specialised platforms (azure, together_ai, nscale) versus cheap commodity resellers (deepinfra, wandb, mistral). The 42 models with spread &amp;gt;200% (11% of n) are almost entirely in this bucket. These are not price errors; they are two different products sold under the same model name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the spread table hides
&lt;/h2&gt;

&lt;p&gt;Three caveats are easy to miss when reading the spread table. First, the prices are per-token input rates at a short context length. Long-context requests—where the model must read a large prompt—are priced differently by many providers. The $0.03/M rate on wandb for &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; likely assumes a standard context length; a long-context request can push that price up by an order of magnitude. The spread table does not capture that, so the 2566.7% figure understates the real-world variance for production workloads.&lt;/p&gt;

&lt;p&gt;Second, the dataset's prices are point-in-time snapshots. Providers change list prices frequently, often weekly. The 13 providers for &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; may have had a $0.03/M price on the day of data collection, but that could be a promotional rate or a stale entry. Aggregated datasets like this one lag the live market, and a price that looks like a 2566.7% outlier today may be a 10% spread next week. The median of 0.0% is robust to this, but the tail is not.&lt;/p&gt;

&lt;p&gt;Third, "the same model" does not always mean bit-identical. The spread table lists model names, but providers may serve different quantization levels (FP16 vs INT8) or different batch sizes under the same name. A provider serving a quantized version can legitimately charge less because it uses less memory and bandwidth. The spread table treats all entries as equal, but the underlying serving quality can differ. This is why the 270 models with &amp;lt;20% spread are the reliable benchmark—those providers are serving the same bits at the same speed. The 42 models with &amp;gt;200% spread are where the model name is the only common denominator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worst spreads, measured
&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;Providers&lt;/th&gt;
&lt;th&gt;Cheapest&lt;/th&gt;
&lt;th&gt;Priciest&lt;/th&gt;
&lt;th&gt;Spread&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-235B-A22B-Instruct-2507&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.09/M (deepinfra)&lt;/td&gt;
&lt;td&gt;$3/M (crusoe)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+3233.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;$0.03/M (wandb)&lt;/td&gt;
&lt;td&gt;$0.8/M (crusoe)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2566.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-ai/DeepSeek-R1-Distill-Qwen-14B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$0.07/M (nscale)&lt;/td&gt;
&lt;td&gt;$1.6/M (together_ai)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2185.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mistral-large-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.5/M (mistral)&lt;/td&gt;
&lt;td&gt;$8/M (azure)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1500.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen2.5-Coder-32B-Instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.06/M (nscale)&lt;/td&gt;
&lt;td&gt;$0.8/M (together_ai)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1233.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-ai/DeepSeek-R1-0528&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$0.25/M (hyperbolic)&lt;/td&gt;
&lt;td&gt;$3/M (crusoe)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1100.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-235B-A22B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.18/M (deepinfra)&lt;/td&gt;
&lt;td&gt;$2/M (hyperbolic)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1011.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;meta-llama/Llama-3.3-70B-Instruct-Turbo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$0.1/M (deepinfra)&lt;/td&gt;
&lt;td&gt;$1.04/M (together_ai)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+940.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; in detail
&lt;/h3&gt;

&lt;p&gt;Served by &lt;strong&gt;13 providers&lt;/strong&gt;, spanning $0.03/M to $0.8/M on input tokens - a &lt;strong&gt;2566.7% spread&lt;/strong&gt;, roughly 27x. Picking a provider at random here is a coin toss between the cheapest and most expensive option in the dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;Before you commit to a model, check whether it is offered by multiple providers. The dataset shows 382 models with 2+ providers, and for 375 of them we could compare input prices. The median spread is 0.0%, but 42 models (11% of that sample) have spreads above 200%. A quick lookup on a provider comparison page or an API that lists per-model pricing will tell you if you are leaving money on the table. For a single model like &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;, the same input token costs $0.03/M on one provider and $0.8/M on another—that is a +2566.7% spread across 13 providers, so the variance is real.&lt;/p&gt;

&lt;p&gt;Switching providers is often a one-line change: most OpenAI-compatible SDKs only need a different &lt;code&gt;base_url&lt;/code&gt; and API key. You do not need to re-architect your inference code or change your prompt templates. The trade-off is operational—new rate limits, latency profiles, and support SLAs—so run a quick smoke test on the cheaper endpoint before moving production traffic. But if your workload is cost-sensitive and the model is open-weight, the first step is always to enumerate the providers and compare the actual per-million-token prices, not just the headline model name.&lt;/p&gt;

&lt;p&gt;Which pricing dimension would you like us to analyse next—batch versus realtime inference pricing, or regional price differences across providers?&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;Parsed the LiteLLM &lt;code&gt;model_cost&lt;/code&gt; table (4033 model entries, 137 providers), grouped entries by underlying model after stripping the provider prefix, kept models served by 2+ providers that had a comparable input price (n=375), and compared minimum against maximum cost per million input tokens. Prices are a snapshot of the dataset, not live quotes.&lt;/p&gt;

&lt;p&gt;The full per-model tables are kept up to date at &lt;a href="https://ifetchora.com/" rel="noopener noreferrer"&gt;ifetchora.com&lt;/a&gt; - every model page lists the per-provider price table, the cheapest option, and the saving from switching. It rebuilds daily from the same source.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
    </item>
    <item>
      <title>Mirror Speed Is a Lie Until You Measure It</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Mon, 19 Oct 2026 00:17:58 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/mirror-speed-is-a-lie-until-you-measure-it-1bkp</link>
      <guid>https://dev.to/street_foodchinese_57726/mirror-speed-is-a-lie-until-you-measure-it-1bkp</guid>
      <description>&lt;h1&gt;
  
  
  Mirror Speed Is a Lie Until You Measure It
&lt;/h1&gt;

&lt;p&gt;oncidentally, I spent last Tuesday afternoon chasing a phantom. A colleague swore our internal NuGet mirror was "slow as hell," and the build server logs backed him up—package restores that used to take 40 seconds were taking four minutes. I opened the mirror's status page, saw green health checks, and immediately suspected the network. Then I actually measured transfer speed from the mirror host to the build agent, and the number was fine: 900 Mbps sustained. The mirror itself wasn't slow. The &lt;em&gt;path&lt;/em&gt; to the mirror was slow. That's the first lesson: mirror speed is never a single number. It's a topology of bottlenecks, and you have to measure each hop before you can fix any of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Numbers That Actually Matter
&lt;/h2&gt;

&lt;p&gt;When someone says "mirror speed," they usually mean one of three things, and conflating them causes most of the pain I see in CI pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Upstream sync latency&lt;/strong&gt; — how stale the mirror is relative to the origin. If you mirror &lt;code&gt;https://repo1.maven.org&lt;/code&gt; once a day, your "speed" is irrelevant when a developer needs a package published 20 minutes ago. I check this with a simple HTTP HEAD request to a known volatile artifact. For Maven, I use the &lt;code&gt;maven-metadata.xml&lt;/code&gt; timestamp. For npm, I hit the registry's &lt;code&gt;-/npm/v1/registry&lt;/code&gt; endpoint and compare &lt;code&gt;last-modified&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Transfer throughput&lt;/strong&gt; — the raw bytes-per-second between the mirror and the consumer. This is what most people benchmark with &lt;code&gt;wget&lt;/code&gt; or &lt;code&gt;curl&lt;/code&gt;, but they forget that TCP window scaling, TLS handshake overhead, and disk cache all distort the result. I always use a file that's at least 50 MB, and I force a cache miss by appending a query string with a random value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Connection establishment latency&lt;/strong&gt; — the round-trip time for a new TCP/TLS connection. This matters more than throughput for package managers that open dozens of small connections (looking at you, pip). A mirror with 200 ms RTT but 10 Gbps throughput will feel slow for &lt;code&gt;pip install&lt;/code&gt; because every package metadata fetch pays the full RTT.&lt;/p&gt;

&lt;p&gt;Here's the PowerShell snippet I use to get all three from a Windows build agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$mirror&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mirror.internal.corp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$testFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mirror&lt;/span&gt;&lt;span class="s2"&gt;/benchmark/large-file.bin"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# 100 MB pre-generated&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Diagnostics.Stopwatch&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$testFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Head&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UseBasicParsing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$rtt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$tempFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="s2"&gt;\mirror_bench_&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;).bin"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$testFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-OutFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tempFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UseBasicParsing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalSeconds&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$sizeMB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tempFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1MB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$throughput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sizeMB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RTT: &lt;/span&gt;&lt;span class="nv"&gt;$rtt&lt;/span&gt;&lt;span class="s2"&gt; ms"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Throughput: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$throughput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;) MB/s"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tempFile&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that from the build agent, not from your laptop. The laptop's VPN path will lie to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real-World Workflow: Diagnosing a Slow Apt Mirror
&lt;/h2&gt;

&lt;p&gt;My most recent battle was with an internal &lt;code&gt;apt-cacher-ng&lt;/code&gt; instance. The symptom: &lt;code&gt;apt-get update&lt;/code&gt; on Ubuntu 22.04 agents took 90 seconds instead of 8. The mirror was on a 10 GbE link, and the agents were on the same VLAN. So why so slow?&lt;/p&gt;

&lt;p&gt;I started by checking the cache hit ratio. &lt;code&gt;apt-cacher-ng&lt;/code&gt; exposes this at &lt;code&gt;http://mirror:3142/acng-report.html&lt;/code&gt;. The hit ratio was 94%, which is healthy. But the &lt;em&gt;miss&lt;/em&gt; path was the problem. When a package isn't cached, &lt;code&gt;apt-cacher-ng&lt;/code&gt; fetches from the upstream Ubuntu archive, and that upstream connection was pegged at 15 Mbps because the corporate firewall was throttling port 80 traffic to non-whitelisted domains.&lt;/p&gt;

&lt;p&gt;The fix wasn't to speed up the mirror. It was to change the upstream URL from &lt;code&gt;http://archive.ubuntu.com&lt;/code&gt; to &lt;code&gt;http://mirror.internal.corp/ubuntu-archive&lt;/code&gt; — an internal proxy that had a fast path. I edited &lt;code&gt;/etc/apt-cacher-ng/acng.conf&lt;/code&gt;, changed &lt;code&gt;Remap-deb1&lt;/code&gt; and &lt;code&gt;Remap-deb2&lt;/code&gt; lines to point to the internal proxy, then restarted the service. &lt;code&gt;apt-get update&lt;/code&gt; dropped to 11 seconds. The mirror was never the problem; the &lt;em&gt;origin&lt;/em&gt; was.&lt;/p&gt;

&lt;p&gt;Here's the Bash snippet I used to verify the upstream speed before and after the change:&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;# Before: direct to upstream&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{speed_download}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://archive.ubuntu.com/ubuntu/dists/jammy/InRelease

&lt;span class="c"&gt;# After: through internal proxy&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{speed_download}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://mirror.internal.corp/ubuntu-archive/dists/jammy/InRelease
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command reported 1.8 MB/s. The second reported 22.4 MB/s. Same file, same agent, same network. The only difference was the route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Modes That Look Like Speed but Aren't
&lt;/h2&gt;

&lt;p&gt;After a few years of this, I've learned to recognize the classic impostors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS session resumption failures.&lt;/strong&gt; If your mirror uses a certificate chain that's too deep, or if the client doesn't support the same TLS version, every connection does a full handshake. That adds 300-500 ms per connection. With 200 package requests, that's 60-100 seconds of pure overhead. The fix is usually to enable TLS 1.3 on both ends and make sure the client has the CA certificate installed. I've seen &lt;code&gt;apt&lt;/code&gt; and &lt;code&gt;pip&lt;/code&gt; both silently fall back to TLS 1.2 and then to 1.0, each handshake getting slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk I/O on the mirror host.&lt;/strong&gt; A mirror that's been running for years accumulates a fragmented filesystem or runs out of inodes. The network says 10 Gbps, but the disk can only serve 50 MB/s random reads. The symptom: large files download fast, but many small files (package metadata) crawl. I check &lt;code&gt;iostat -x 1&lt;/code&gt; on the mirror host and look for &lt;code&gt;await&lt;/code&gt; times over 50 ms. The fix is often just &lt;code&gt;xfs_fsr&lt;/code&gt; or a periodic &lt;code&gt;fstrim&lt;/code&gt; if you're on SSDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side DNS resolution.&lt;/strong&gt; This one is sneaky. The build agent's DNS resolver might take 2 seconds to resolve &lt;code&gt;mirror.internal.corp&lt;/code&gt; because the DNS server is slow or the record has a long TTL and the local cache expired. I've seen &lt;code&gt;pip install&lt;/code&gt; spend 30 seconds just resolving the hostname for each package. The fix: add a static entry in &lt;code&gt;C:\Windows\System32\drivers\etc\hosts&lt;/code&gt; on the agents. This is a one-line change that often "fixes" a slow mirror without touching the mirror at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP keep-alive misuse.&lt;/strong&gt; Some package managers (older versions of &lt;code&gt;nuget.exe&lt;/code&gt;) don't reuse connections. They open a new TCP connection per package. If the mirror is behind a load balancer that doesn't support keep-alive well, you get connection churn. I've fixed this by adding a reverse proxy with &lt;code&gt;proxy_http_version 1.1&lt;/code&gt; and &lt;code&gt;proxy_set_header Connection ""&lt;/code&gt; in nginx.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Case: The Mirror That's Too Fast
&lt;/h2&gt;

&lt;p&gt;Here's a counterintuitive one. A mirror that's &lt;em&gt;too fast&lt;/em&gt; can cause timeouts. I once set up a local npm mirror with an in-memory cache. It served packages in under 1 ms. But &lt;code&gt;npm&lt;/code&gt; on the client side has a built-in timeout for the initial handshake. When the server responds faster than the client's TCP stack can process, you get sporadic &lt;code&gt;ECONNRESET&lt;/code&gt; errorscars. The fix was to add a small artificial delay on the mirror side (10 ms) using &lt;code&gt;tc qdisc add dev eth0 root netem delay 10ms&lt;/code&gt;. That sounds insane, but it stabilized the client. Sometimes you have to slow down to speed up.&lt;/p&gt;

&lt;p&gt;Another edge case: mirroring to a network filesystem (NFS or SMB). The mirror process writes to a network share, and the share's latency dominates. I saw a &lt;code&gt;rsync&lt;/code&gt; mirror to a CIFS share that took 45 minutes for a 2 GB update because every file write had a 50 ms round-trip to the file server. The fix was to sync to local disk first, then &lt;code&gt;rsync&lt;/code&gt; the local disk to the remote share in a second pass Mend the write latency only affects the final copy, not the mirror's read path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the Right Thing in CI
&lt;/h2&gt;

&lt;p&gt;If you're running a CI farm, the most useful metric is not "mirror throughput" but "package restore time percentile." I add a step to every pipeline that times &lt;code&gt;dotnet restore&lt;/code&gt; or &lt;code&gt;pip install&lt;/code&gt; and logs the 95th percentile. If that number creeps up, I don't look at the mirror first. I look at the agent's CPU count, disk type, and whether the agent is sharing a VM host with a noisy neighbor.&lt;/p&gt;

&lt;p&gt;Here's a PowerShell snippet I use to benchmark the full restore path on a Windows agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Diagnostics.Stopwatch&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\src\MyApp\MyApp.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://mirror.internal.corp/v3/index.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalSeconds&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$pkgCount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.nupkg"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Restore took &lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="s2"&gt; s for &lt;/span&gt;&lt;span class="nv"&gt;$pkgCount&lt;/span&gt;&lt;span class="s2"&gt; packages"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Per-package time: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pkgCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;) s"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that per-package time is over 0.5 seconds, something's wrong. On a good mirror, it should be under 0.1 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;If you're wrestling with mirror speed on Windows, you might also be dealing with the broader problem of managing downloaded resources efficiently. I've been using &lt;a href="https://gitvaults.com/download/2025/12/30/putyy-res-downloader-3-1-3/en" rel="noopener noreferrer"&gt;putyy res downloader 3 1 3&lt;/a&gt; from GitVaults to batch-download and verify large resource files before pushing them to my mirrors. It handles resumable downloads and checksum validation, which saves me from re-downloading a corrupted 2 GB file because a flaky mirror dropped the last 5 MB. GitVaults is a solid catalog for Windows open-source tools, and their download pages are clean without the usual ad-riddled wrappers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;This is all based on my own experience with specific networks, mirrors, and clients. Your mileage will vary. I've seen cases where the exact opposite of my advice was true—for example, a mirror that was slow because the &lt;em&gt;client&lt;/em&gt; was on a throttled Wi-Fi link, not because of anything on the server side. If you spot an error in my methodology, or you have a better trick for isolating mirror bottlenecks, please correct me. I'm always learning from people who measure more carefully than I do.&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>networking</category>
      <category>performance</category>
      <category>sre</category>
    </item>
    <item>
      <title>Mirror Speed: What Actually Matters When You're Pulling from a Repo at 3 AM</title>
      <dc:creator>street food Chinese</dc:creator>
      <pubDate>Sat, 17 Oct 2026 23:43:11 +0000</pubDate>
      <link>https://dev.to/street_foodchinese_57726/mirror-speed-what-actually-matters-when-youre-pulling-from-a-repo-at-3-am-12mo</link>
      <guid>https://dev.to/street_foodchinese_57726/mirror-speed-what-actually-matters-when-youre-pulling-from-a-repo-at-3-am-12mo</guid>
      <description>&lt;h1&gt;
  
  
  Mirror Speed: What Actually Matters When You're Pulling from a Repo at 3 AM
&lt;/h1&gt;

&lt;p&gt;I’ve spent more hours than I care to admit staring at a terminal, waiting for a &lt;code&gt;git clone&lt;/code&gt; to finish. The progress bar crawls, the network indicator sits at a trickle, and I start questioning every DNS setting I’ve ever touched. Mirror speed is one of those topics that sounds simple—"just pick a faster mirror"—but in practice, it’s a tangle of protocol overhead, geographic routing, and filesystem cache behavior. Let me walk you through what I’ve learned from managing my own local mirror of a few large open-source Windows tool repositories, and where I’ve hit the wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Mistake: Assuming "Faster" Means "Closer"
&lt;/h2&gt;

&lt;p&gt;The most common advice is to pick a mirror near you. That’s not wrong, but it’s incomplete. I once had a mirror in the same city that was consistently slower than one two time zones away. Why? Because the local mirror was behind a shared 1 Gbps link that was saturated by a CDN serving video streams. The distant mirror had a dedicated 10 Gbps pipe and a better TCP stack.&lt;/p&gt;

&lt;p&gt;Here’s the thing: mirror speed is a function of &lt;strong&gt;end-to-end throughput&lt;/strong&gt;, not just latency. You can measure it with a simple test. On Windows, I use PowerShell to download a known file from each candidate mirror and time it. But you need to be careful about caching—if you hit the same file twice, the OS file cache will skew the results)Skip the file cache by using a unique query string or a different file each time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Quick mirror throughput test on Windows&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://mirror-a.example.com/file1.bin?cachebust=1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://mirror-b.example.com/file1.bin?cachebust=2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Diagnostics.Stopwatch&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-OutFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="s2"&gt;\test.bin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UseBasicParsing&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="s2"&gt;\test.bin"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$mbps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalSeconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;MB&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$mbps&lt;/span&gt;&lt;span class="s2"&gt; Mbps"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="s2"&gt;\test.bin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But even this has a flaw. A single file test doesn’t reflect the reality of a &lt;code&gt;git clone&lt;/code&gt; or a package manager update, which opens hundreds of small connections. That’s where the real bottleneck often hides: &lt;strong&gt;connection setup overhead&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallelism and the Hidden Cost of Connection Setup
&lt;/h2&gt;

&lt;p&gt;When you pull from a mirror, you’re not downloading one big file. You’re downloading thousands of small objects—blobs, trees, commit metadata. Each one may require a separate HTTP request. The TCP handshake, TLS negotiation, and HTTP headers can add 50–100 ms per request. If you have 10,000 objects, that’s 500–1000 seconds of pure overhead, even if your bandwidth is infinite.&lt;/p&gt;

&lt;p&gt;That’s why I stopped using plain &lt;code&gt;git clone&lt;/code&gt; from mirrors and started using &lt;strong&gt;shallow clones&lt;/strong&gt; (&lt;code&gt;--depth 1&lt;/code&gt;) for CI and quick testing. But for a full mirror, that’s not an option. So I turned to tools that support &lt;strong&gt;concurrent downloads&lt;/strong&gt;. The most reliable I’ve found is &lt;code&gt;aria2&lt;/code&gt; (which works well on Windows via &lt;code&gt;choco install aria2&lt;/code&gt;), but the real game-changer for me was a small utility called &lt;code&gt;putyy res downloader 3 1 3&lt;/code&gt;. It’s designed specifically to fetch resource packs from multiple mirrors in parallel and then reassemble them, which is exactly the pattern that kills naive single-threaded downloaders.&lt;/p&gt;

&lt;p&gt;Here’s a typical workflow I use when I need to refresh a large mirror of a Windows SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Using aria2c to pull a large manifest with 16 connections&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;aria2c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;16&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;16&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sdk-mirror.zip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"https://mirror-a.example.com/sdk/latest.zip"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"https://mirror-b.example.com/sdk/latest.zip"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"https://mirror-c.example.com/sdk/latest.zip"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice I’m passing multiple URLs for the same file. &lt;code&gt;aria2c&lt;/code&gt; will stripe the download across all of them, and if one mirror fails mid-transfer, it retries from another. That’s the kind of resilience you need when a mirror goes flaky at 2 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Modes You’ll Actually Hit (and How to Spot Them)
&lt;/h2&gt;

&lt;p&gt;I’ve seen three distinct failure modes that kill mirror speed, and none of them show up in a simple ping test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The "Stale Cache" Mirror.&lt;/strong&gt; Some mirrors use a reverse proxy with a long TTL. You request a file, and the proxy serves you a cached version from yesterday—but the actual upstream file has changed. The download is fast, but you get corrupted or outdated data. The symptom: &lt;code&gt;git clone&lt;/code&gt; succeeds, but &lt;code&gt;git fsck&lt;/code&gt; reports missing objects. The fix: always verify checksums. I write a small script that compares the SHA256 of downloaded files against a manifest from the upstream repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The "Throttled by IP" Mirror.&lt;/strong&gt; Some mirrors throttle you after the first 100 MB if you’re on a shared IP (like a corporate NAT). The speed starts high, then drops to a crawl. The symptom is a classic "hockey stick" throughput curve. The workaround: rotate through mirrors using a tool like &lt;code&gt;putyy res downloader 3 1 3&lt;/code&gt;, which can be configured to switch mirrors when throughput drops below a threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The "IPv6 Blackhole" Mirror.&lt;/strong&gt; This one is subtle. Your machine prefers IPv6, but the mirror’s IPv6 route is broken—packets are dropped, not rejected. TCP retransmits kill your speed. The symptom: the download hangs for 10 seconds, then bursts. The fix: force IPv4 with &lt;code&gt;curl -4&lt;/code&gt; or set &lt;code&gt;prefer_ipv4=1&lt;/code&gt; in your &lt;code&gt;~/.gitconfig&lt;/code&gt; if you’re using git over HTTP.&lt;/p&gt;

&lt;p&gt;Here’s a concrete PowerShell snippet I use to detect the IPv6 issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check if IPv6 to a mirror is actually working&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Test-NetConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ComputerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mirror-a.example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AddressFamily&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ipv6&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TcpTestSucceeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IPv6 OK, but let's measure real throughput..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Diagnostics.Stopwatch&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;curl.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;NUL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mirror-a.example.com/test.bin"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IPv6 download took &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalSeconds&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; sec"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IPv6 is broken, forcing IPv4"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Edge Case: The Local Filesystem Is the Real Bottleneck
&lt;/h2&gt;

&lt;p&gt;I nearly pulled my hair out once because a mirror was "slow," but it turned out the mirror was fine—my local disk was the problem. I was downloading to a spinning HDD that was also serving a virtual machine’s pagefile. The disk I/O queue was so deep that writes were taking 200 ms per block. The download was actually arriving at the network card at full speed, but the OS couldn’t write it to disk fast enough.&lt;/p&gt;

&lt;p&gt;The fix was simple: download to an SSD or RAM disk. On Windows, I use &lt;code&gt;New-VHD&lt;/code&gt; to create a small RAM-backed drive, but that’s overkill. More practically, I just pointed my temp directory to a fast NVMe drive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Redirect temp downloads to fast storage&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\FastTemp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TMP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\FastTemp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, check your antivirus. Real-time scanning on every downloaded file can halve your throughput. I exclude my mirror directories from Windows Defender’s real-time scanning. That alone sped up my pulls by 30–40% on a large repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Workflow: What I Do Now
&lt;/h2&gt;

&lt;p&gt;My current workflow for keeping a local mirror of a large Windows toolchain is this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Maintain a list of 5–6 candidate mirrors&lt;/strong&gt; across different ISPs and regions. I update this list monthly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a segmented downloader&lt;/strong&gt; (either &lt;code&gt;aria2c&lt;/code&gt; or &lt;code&gt;putyy res downloader 3 1 3&lt;/code&gt;) that can parallelize and failover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always verify checksums&lt;/strong&gt; after download. I keep a &lt;code&gt;SHA256SUMS&lt;/code&gt; file in the mirror root and run a PowerShell verification script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor throughput over time&lt;/strong&gt;, not just once. I log the speed of each mirror in a CSV and look for trends. A mirror that was fast last week might be saturated this week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use shallow clones for ephemeral tasks&lt;/strong&gt;, and only do a full mirror when I actually need the history.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tool &lt;code&gt;putyy res downloader 3 1 3&lt;/code&gt; has become a regular part of this because it handles the failover logic automatically. It doesn’t just download—it tracks which mirror responded fastest for the last N requests and biases future requests toward that one. That’s the kind of adaptive behavior you don’t get from a simple &lt;code&gt;wget&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related resource
&lt;/h2&gt;

&lt;p&gt;If you’re working with Windows resource packs and need a downloader that handles multi-mirror fallback gracefully, I’ve found &lt;a href="https://gitvaults.com/download/2025/12/30/putyy-res-downloader-3-1-3/en" rel="noopener noreferrer"&gt;putyy res downloader 3 1 3&lt;/a&gt; to be a solid addition to the toolkit. It’s hosted on GitVaults, which is a catalog of open-source Windows software—useful for finding tools like this without wading through random forum posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;I’ve shared what works for me, but mirror speed is highly environment-specific. Your ISP’s peering, your local hardware, and even the time of day can flip which mirror is fastest. If you see something in my approach that’s wrong or have a better trick—especially around Windows-specific network stack tuning—I’d genuinely appreciate a correction. Drop a note in the comments or reach out; I’m always learning from people who’ve hit different failure modes than I have.&lt;/p&gt;

</description>
      <category>git</category>
      <category>infrastructure</category>
      <category>networking</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
