<?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: Susumu Takahashi</title>
    <description>The latest articles on DEV Community by Susumu Takahashi (@susumun).</description>
    <link>https://dev.to/susumun</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%2F3961116%2F87a59747-8eb8-43eb-9db6-c160d3592934.JPG</url>
      <title>DEV Community: Susumu Takahashi</title>
      <link>https://dev.to/susumun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/susumun"/>
    <language>en</language>
    <item>
      <title>DNS Records Explained — What A, CNAME, and TXT Records Actually Point To</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Wed, 30 Sep 2026 00:40:54 +0000</pubDate>
      <link>https://dev.to/susumun/dns-records-explained-what-a-cname-and-txt-records-actually-point-to-3of6</link>
      <guid>https://dev.to/susumun/dns-records-explained-what-a-cname-and-txt-records-actually-point-to-3of6</guid>
      <description>&lt;p&gt;"Configuring a domain" usually boils down to "adding a DNS record." Setting up a subdomain, improving email deliverability, proving ownership of a domain to some external service — the goals differ, but the actual task is often the same: add one line in a domain management panel. This post walks through three of the most common DNS record types — A, CNAME, and TXT — what each one actually points to, and two real examples from our own domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DNS is resolving
&lt;/h2&gt;

&lt;p&gt;Between typing &lt;code&gt;wpmm.jp&lt;/code&gt; into a browser and a page actually loading, there's a lookup happening: "this human-readable name — which server's IP address does it actually correspond to?" DNS (the Domain Name System) is the distributed database that answers that question, organizing information per domain into units called records. Records come in different types, and each type answers a different kind of question. Here we'll focus on the three that come up most often.&lt;/p&gt;

&lt;h2&gt;
  
  
  A records — a hostname pointing straight at an IPv4 address
&lt;/h2&gt;

&lt;p&gt;The A record is the most basic type: it maps a hostname (say, &lt;code&gt;wpmm.jp&lt;/code&gt;) directly to a specific IPv4 address. It's a simple map — "traffic to this domain should go to this IP." When you stand up a new web server and want to serve it from a custom domain, an A record is typically the first thing you configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  CNAME records — a hostname pointing at another hostname
&lt;/h2&gt;

&lt;p&gt;A CNAME (Canonical Name) record treats one hostname as an alias for another. Instead of pointing directly at an IP address, it says "the real content for this name lives over there, at that other hostname" — an indirect reference, unlike the direct one an A record gives you. CNAME comes with a few constraints worth knowing. The most important: a name that has a CNAME record can't also carry other record types (like MX or TXT) at the same time, because a CNAME forwards &lt;em&gt;every&lt;/em&gt; query for that name wholesale. It also can't be placed at a zone apex (the bare domain itself, like &lt;code&gt;wpmm.jp&lt;/code&gt; with no subdomain) — an A record is the usual choice there instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  TXT records — arbitrary text attached to a name
&lt;/h2&gt;

&lt;p&gt;A TXT record attaches an arbitrary text string to a domain, rather than an IP address or another hostname. It started out as a place for human-readable notes, but today it's almost entirely consumed by machines. Two common uses: proving ownership of a domain to an external service (the service issues a random string, you add it as a TXT record, and that proves you control the DNS for that domain), and email authentication configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: standing up a subdomain starts with an A record
&lt;/h2&gt;

&lt;p&gt;When we set up our English blog at the subdomain &lt;code&gt;en.wpmm.jp&lt;/code&gt;, the first step was creating that subdomain in the hosting panel and pointing it at a document root. Behind the scenes, that action adds an A record for the new hostname &lt;code&gt;en.wpmm.jp&lt;/code&gt; (or configures the hosting provider's authoritative DNS to resolve it implicitly) — and it takes some time for that change to reach DNS caches around the world, commonly called "propagation." That delay has a very practical consequence: try to install WordPress on the new subdomain immediately after creating it, and a connection attempt from an environment still holding a stale cache entry can fail outright. Right after a DNS change, it's worth confirming through a separate path — a &lt;code&gt;dig&lt;/code&gt; lookup, or checking against a different resolver — that the change has actually propagated, before moving on to the next step. It saves time that would otherwise go into chasing a mysterious error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 2: email deliverability comes down to a set of three TXT records
&lt;/h2&gt;

&lt;p&gt;When we investigated why our confirmation emails weren't reliably reaching Gmail and Outlook inboxes, the fix that actually mattered was adding a TXT record. Email authentication rests on three mechanisms — SPF, DKIM, and DMARC — and all three are published as TXT records. SPF lists which servers are allowed to send mail on behalf of a domain. DKIM attaches a cryptographic signature to a message so a recipient can verify it wasn't tampered with in transit. DMARC declares a policy for what to do when SPF and/or DKIM checks fail, and where to send reports about those results. Our domain already had SPF and DKIM configured; DMARC was the one missing piece. Adding a TXT record at the hostname &lt;code&gt;_dmarc.wpmm.jp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;v=DMARC1; p=none; rua=mailto&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt;info@wpmm.jp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;completed the set of three, and deliverability to major overseas mail providers improved. (&lt;code&gt;p=none&lt;/code&gt; is the loosest possible policy — it means "don't reject or quarantine mail that fails authentication, just send a summary report" — a deliberate way to observe before escalating to something stricter like &lt;code&gt;p=reject&lt;/code&gt;.) We go into more detail on how SPF, DKIM, and DMARC divide the work in &lt;a href="https://en.wpmm.jp/blog/spf-dkim-dmarc-email-deliverability/" rel="noopener noreferrer"&gt;our earlier post on this exact fix&lt;/a&gt;, if you want the fuller picture.&lt;/p&gt;

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

&lt;p&gt;An A record answers "what IP does this name point to." A CNAME answers "what other name does this name point to." A TXT record answers "what arbitrary text is attached to this name." Each type is answering a genuinely different question. In practice, the part that trips people up isn't the meaning of the record itself — it's the time lag between making a change and that change actually taking effect anywhere it's queried from. For anything involving multiple steps, like standing up a subdomain or wiring up email authentication, confirming that a DNS change has actually propagated before moving to the next step tends to be the faster path, not the slower one.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>pip Version Specifiers — What `==`, `&gt;=`, and `~=` Actually Commit You To</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 00:52:54 +0000</pubDate>
      <link>https://dev.to/susumun/pip-version-specifiers-what-and-actually-commit-you-to-5a9</link>
      <guid>https://dev.to/susumun/pip-version-specifiers-what-and-actually-commit-you-to-5a9</guid>
      <description>&lt;p&gt;Anyone who has written a &lt;code&gt;requirements.txt&lt;/code&gt; file has probably paused at the line after the package name. Leave it blank, pin it with &lt;code&gt;==3.0.0&lt;/code&gt;, set a floor with &lt;code&gt;&amp;gt;=3.0.0&lt;/code&gt;, or split the difference with &lt;code&gt;~=3.0.0&lt;/code&gt; — they look similar, but each one makes a completely different promise about what gets installed in the future. This post walks through pip's version specifier syntax, then looks at why our own tool's &lt;code&gt;requirements.txt&lt;/code&gt; picked one particular style, and what that choice trades away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question a version specifier is answering
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: pip's version specifiers follow &lt;a href="https://peps.python.org/pep-0440/" rel="noopener noreferrer"&gt;PEP 440&lt;/a&gt;. Writing an operator and a version number after a package name tells pip which range of versions is acceptable to install.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Leave the specifier off entirely, and pip installs whatever the latest release happens to be at install time. That's a statement of "any current version is fine" — but running &lt;code&gt;pip install&lt;/code&gt; against the same &lt;code&gt;requirements.txt&lt;/code&gt; six months later will pull in whatever the ecosystem has moved on to by then. Version specifiers exist to put a boundary around that uncertainty: how much freedom do you want to give the installer over what "the future" is allowed to look like?&lt;/p&gt;

&lt;h2&gt;
  
  
  The main operators, compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==3.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only this exact version is allowed (fully pinned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=3.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This version or newer — a floor only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;3.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only versions below this — a ceiling only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!=3.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any version except this one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~=3.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3.0.0 or newer, but below 3.1.0 (the "compatible release" operator)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one that trips people up most is &lt;code&gt;~=&lt;/code&gt;. &lt;code&gt;~=3.0.0&lt;/code&gt; is shorthand for &lt;code&gt;&amp;gt;=3.0.0, ==3.0.*&lt;/code&gt; — it accepts patch-level updates (the third number) but locks out the moment the minor version (the second number) ticks up. Truncate it to &lt;code&gt;~=3.0&lt;/code&gt; instead, and the tolerance widens to accept minor updates too, becoming the equivalent of &lt;code&gt;&amp;gt;=3.0, ==3.*&lt;/code&gt;. How many digits you write changes the granularity of what's allowed — that's the real difference from a plain &lt;code&gt;&amp;gt;=&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why our own requirements.txt uses &lt;code&gt;&amp;gt;=&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Our desktop maintenance tool's &lt;code&gt;requirements.txt&lt;/code&gt; specifies all six dependencies with a lower bound only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flask&amp;gt;=3.0.0
fabric&amp;gt;=3.2.0
playwright&amp;gt;=1.40.0
cryptography&amp;gt;=41.0.0
Pillow&amp;gt;=10.0.0
certifi&amp;gt;=2024.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We chose &lt;code&gt;&amp;gt;=&lt;/code&gt; over a full &lt;code&gt;==&lt;/code&gt; pin so that security and bug fixes in newer releases get picked up automatically. That matters especially for &lt;code&gt;cryptography&lt;/code&gt; (crypto primitives) and &lt;code&gt;certifi&lt;/code&gt; (the CA certificate bundle), both of which receive periodic vulnerability fixes and root-certificate updates — pinning either one indefinitely to an old version is itself a risk. Leaving off an upper bound follows the same logic: nothing rules out a future release before it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;&amp;gt;=&lt;/code&gt; gives up in exchange — build reproducibility
&lt;/h2&gt;

&lt;p&gt;The trade-off shows up at build time. This tool is packaged into a distributable binary with &lt;code&gt;arch -x86_64 pip3 install -r requirements.txt &amp;amp;&amp;amp; arch -x86_64 python3 build_app.py&lt;/code&gt;, using PyInstaller. Run that exact command twice, weeks apart, without touching &lt;code&gt;requirements.txt&lt;/code&gt; at all, and pip resolves "3.0.0 or newer" fresh each time — so the two builds can end up bundling genuinely different versions of &lt;code&gt;flask&lt;/code&gt; or &lt;code&gt;cryptography&lt;/code&gt;. Looking at a diff of &lt;code&gt;requirements.txt&lt;/code&gt; alone can't tell you whether two builds actually shipped with the same dependency set.&lt;/p&gt;

&lt;p&gt;That uncertainty becomes a real problem when a bug only reproduces on one specific dependency version, or when a minor-version bump quietly changes behavior — a deprecation warning gets promoted to a hard error, a default value flips — and it slips into a build unnoticed because nothing in the repo changed. Full &lt;code&gt;==&lt;/code&gt; pinning would turn "update a dependency" into a deliberate, tested step. With &lt;code&gt;&amp;gt;=&lt;/code&gt;, the update timing is implicitly decided the moment someone runs &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Projects that need strict reproducibility typically solve this with a two-tier setup: keep the loose &lt;code&gt;requirements.txt&lt;/code&gt; for expressing intent, but also generate a lock file from &lt;code&gt;pip freeze&lt;/code&gt; that every routine install actually reads from, regenerating it only when a dependency bump is deliberate. Our tool runs with a small team and a mostly static build environment, and dependency updates are infrequent enough that we haven't introduced that second tier — we run on &lt;code&gt;&amp;gt;=&lt;/code&gt; alone for now. If the build environment grows, or subtle version drift between builds starts causing real problems, a lock file is the natural next step.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;==&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt;, and &lt;code&gt;~=&lt;/code&gt; are all answers to the same underlying question: how much should the future version be allowed to drift from what you tested against? &lt;code&gt;==&lt;/code&gt; maximizes reproducibility at the cost of manual updates; &lt;code&gt;&amp;gt;=&lt;/code&gt; picks up updates automatically at the cost of no longer being able to tell, from the requirements file alone, exactly what's installed right now. We picked &lt;code&gt;&amp;gt;=&lt;/code&gt; mainly to avoid missing updates to crypto- and certificate-related packages — but that choice is inseparable from accepting a lower bar on reproducibility. Which operator you reach for is really a design decision about whether your project values staying current or staying reproducible more.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Cache-Control vs. ETag: What Each One Actually Controls</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sun, 27 Sep 2026 23:18:11 +0000</pubDate>
      <link>https://dev.to/susumun/cache-control-vs-etag-what-each-one-actually-controls-4d1d</link>
      <guid>https://dev.to/susumun/cache-control-vs-etag-what-each-one-actually-controls-4d1d</guid>
      <description>&lt;p&gt;Open a browser's network tab and you'll see &lt;code&gt;Cache-Control&lt;/code&gt; and &lt;code&gt;ETag&lt;/code&gt; sitting on the response headers of nearly every image, stylesheet, and script. Most developers recognize both names, but far fewer could explain how they actually divide the work of caching between them. This post walks through the basics of HTTP caching, then looks at how this project's own OGP image generator uses one of these headers and deliberately skips the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem These Headers Solve
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: HTTP caching lets a browser (or a CDN in between) hold on to a response it already fetched — an image, a stylesheet, a script — and reuse it on the next request for the same URL instead of asking the server again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without any instructions from the server, a browser has to guess whether a cached response is still safe to reuse, and that guess tends to be inconsistent across browsers and situations. &lt;code&gt;Cache-Control&lt;/code&gt; and &lt;code&gt;ETag&lt;/code&gt; exist to remove the guesswork by having the server state its intent explicitly. But they answer two different questions: &lt;code&gt;Cache-Control&lt;/code&gt; answers "how long can you skip asking me entirely?" and &lt;code&gt;ETag&lt;/code&gt; answers "once that period is over, how can you cheaply check whether anything actually changed?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cache-Control Controls
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Cache-Control&lt;/code&gt; sets an expiration window. This project's blog theme includes &lt;code&gt;ogp-generator.php&lt;/code&gt;, which renders an OGP preview image with PHP's GD library for any post that has no featured image set. When it serves the generated PNG, it attaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: image/png'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Length: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;filesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cache-Control: public, max-age=2592000, immutable'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each directive does something distinct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;public&lt;/code&gt;&lt;/strong&gt; — this response can be cached not just by the requesting browser but by any shared cache along the way, such as a CDN. (A response with per-user content, like a personal dashboard, would use &lt;code&gt;private&lt;/code&gt; instead.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max-age=2592000&lt;/code&gt;&lt;/strong&gt; — the number of seconds the cache is considered fresh. 2,592,000 seconds is exactly 30 days; for that entire window, a browser revisiting the same URL never contacts the server at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;immutable&lt;/code&gt;&lt;/strong&gt; — a declaration that the content will not change for the duration of &lt;code&gt;max-age&lt;/code&gt;. With this set, the browser skips even the lightweight revalidation check described below on a page reload — it just uses the cached copy, no questions asked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pairing a 30-day window with &lt;code&gt;immutable&lt;/code&gt; only makes sense if "this exact URL will never point to different content" genuinely holds. How that guarantee gets built is the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ETag Controls — Checking In After Expiration
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;max-age&lt;/code&gt; expires, a browser doesn't simply throw the cached copy away — it asks the server whether the old copy is still good. &lt;code&gt;ETag&lt;/code&gt; (short for Entity Tag) is what makes that check cheap.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: an ETag is a short identifier — usually a hash — generated from a response's actual content. If even a single byte of the content changes, the ETag changes with it, making it effectively a fingerprint of the content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The revalidation flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The server's first response includes something like &lt;code&gt;ETag: "abc123"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The browser remembers that value, and once the cached copy has expired, it sends a follow-up request carrying &lt;code&gt;If-None-Match: "abc123"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The server recomputes the content's current ETag. If it hasn't changed, it replies with &lt;strong&gt;&lt;code&gt;304 Not Modified&lt;/code&gt;&lt;/strong&gt; — a lightweight response with no body.&lt;/li&gt;
&lt;li&gt;The browser sees the 304 and keeps using its existing cached copy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key detail is that a &lt;code&gt;304 Not Modified&lt;/code&gt; response never re-transmits the actual image or file — the server still has to do the work of recomputing whether the content changed, but the network transfer itself is skipped. Where &lt;code&gt;max-age&lt;/code&gt; is a blunt instrument ("skip checking entirely for this long"), &lt;code&gt;ETag&lt;/code&gt; is a finer one ("even after the window closes, skip re-downloading if nothing actually changed").&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ogp-generator.php Doesn't Use ETag
&lt;/h2&gt;

&lt;p&gt;Given the above, it might seem like &lt;code&gt;ogp-generator.php&lt;/code&gt; is missing something by not implementing &lt;code&gt;ETag&lt;/code&gt;. Looking at the actual code shows the opposite: the design makes revalidation unnecessary in the first place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$modkey&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post_modified_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'YmdHis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// GMT-based&lt;/span&gt;
&lt;span class="nv"&gt;$cache_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cache_dir&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/post-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$lang&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$modkey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated filename bakes in the post ID, the language, and the post's &lt;strong&gt;last-modified timestamp&lt;/strong&gt;. When a post is edited and &lt;code&gt;post_modified&lt;/code&gt; changes, the resulting PNG's filename — and therefore its URL — changes along with it. This is a technique commonly called cache busting: instead of asking "has this URL's content changed?", the system makes sure a given URL's content can never change in the first place, because any real change produces a different URL.&lt;/p&gt;

&lt;p&gt;Under that design, there's nothing for &lt;code&gt;ETag&lt;/code&gt; to check — a given URL is guaranteed, by its own structure, to point at content that was frozen the moment it was generated. That guarantee is exactly what makes it safe to attach &lt;code&gt;immutable&lt;/code&gt;. If the generator instead reused the same filename after a post's title changed, &lt;code&gt;immutable&lt;/code&gt; would be lying to the browser, and editors would see stale OGP images persist for up to 30 days after every edit.&lt;/p&gt;

&lt;p&gt;Both approaches solve the same underlying problem — keeping a cache from serving stale content — but from opposite directions. &lt;code&gt;ETag&lt;/code&gt; accepts that the URL stays fixed and pays a small revalidation cost on every expiration cycle to confirm freshness. Baking a version into the URL itself avoids that round trip entirely, at the cost of only working cleanly when there's a reliable signal (here, &lt;code&gt;post_modified&lt;/code&gt;) to version against. Which approach fits depends on how often, and at what granularity, the underlying content actually changes.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;Cache-Control&lt;/code&gt; and &lt;code&gt;ETag&lt;/code&gt; both exist to control caching, but they control different parts of it. &lt;code&gt;Cache-Control&lt;/code&gt;'s &lt;code&gt;max-age&lt;/code&gt; controls how long a browser can skip contacting the server entirely. &lt;code&gt;ETag&lt;/code&gt; controls how cheaply the server and browser can confirm, once that window closes, whether the content has genuinely changed. Baking a content version into the URL itself — as &lt;code&gt;ogp-generator.php&lt;/code&gt; does with a post's modification timestamp — sidesteps the need for &lt;code&gt;ETag&lt;/code&gt; altogether, making a long &lt;code&gt;max-age&lt;/code&gt; with &lt;code&gt;immutable&lt;/code&gt; safe rather than reckless. Whichever strategy is used, the caching logic is only as trustworthy as the assumption it's built on about when the underlying content actually changes.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Exponential Backoff vs. Fixed-Interval Retries: When Growing Wait Times Actually Help</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sat, 26 Sep 2026 00:26:28 +0000</pubDate>
      <link>https://dev.to/susumun/exponential-backoff-vs-fixed-interval-retries-when-growing-wait-times-actually-help-1dj</link>
      <guid>https://dev.to/susumun/exponential-backoff-vs-fixed-interval-retries-when-growing-wait-times-actually-help-1dj</guid>
      <description>&lt;p&gt;How to retry a failed operation is a design question every network-facing piece of code eventually has to answer. The textbook technique is exponential backoff — doubling the wait time on each retry — but it isn't automatically the right answer everywhere. This post works through what problem exponential backoff actually solves, then looks at three real retry paths in this app's own code where fixed intervals were chosen instead, and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Exponential Backoff Solves
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: exponential backoff is a retry strategy where the wait time between attempts grows exponentially — 1s, 2s, 4s, 8s, and so on — instead of staying constant.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: wait &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# attempt 0: wait 1s
# attempt 1: wait 2s
# attempt 2: wait 4s
# attempt 3: wait 8s
# attempt 4: wait 16s
# attempt 5: wait 32s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exponential backoff earns its keep in one specific situation: &lt;strong&gt;many independent clients retrying against the same shared resource at once.&lt;/strong&gt; When a server starts failing under load, and every client retries at a fixed interval, the retries arrive in synchronized waves that keep hitting the already-struggling server at the same cadence — a pattern often called a "thundering herd." Spacing each client's retries out exponentially means those waves drift apart over time, giving the server room to recover. That's why most HTTP client libraries and cloud SDKs ship exponential backoff by default.&lt;/p&gt;

&lt;p&gt;The flip side is that this only pays off when many independent clients are actually contending for the same resource. Where that condition doesn't hold, exponential backoff adds complexity without buying anything. Here are three retry sites in this codebase where it was deliberately skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: A Single Fixed Retry for HTTP Checks
&lt;/h2&gt;

&lt;p&gt;This app checks a site's HTTP status before and after WordPress updates, rolling back if things got worse. The core of that logic is &lt;code&gt;maintenance_agent.py::_http_status_check_stable()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_http_status_check_stable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basic_auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_http_status_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basic_auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;basic_auth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_time&lt;/span&gt;
            &lt;span class="n"&gt;_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_http_status_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basic_auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;basic_auth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;status == 0&lt;/code&gt; (a failure before the response even reaches the server — DNS blip, TLS handshake failure, a momentary connection drop) triggers a retry, and it's exactly one retry after a flat 3-second wait. A 5xx response isn't retried at all — once a response actually comes back, the code treats that as a genuine server-side problem rather than a transient network hiccup.&lt;/p&gt;

&lt;p&gt;There's no exponential growth anywhere in this function, and the reason is in a comment elsewhere in the same file: this function gets called from up to five different call sites for a single site during one maintenance run — a baseline check, a post-core-update check, post-rollback checks, and a check after each individual plugin update. On a site with twenty plugins, that call count adds up fast. If each of those calls retried multiple times with growing delays, the total time a single site's maintenance run could take would become unpredictable, which matters a lot for an unattended scheduled run where per-site duration needs to stay roughly bounded. Telling apart "a momentary blip" from "a genuinely down server" doesn't require patiently waiting longer and longer — one fixed retry is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 2: Polling for a File Lock at a Fixed Interval
&lt;/h2&gt;

&lt;p&gt;Two processes in this app — the GUI web server and a background maintenance run launched as a separate process — can both try to write the same config file (&lt;code&gt;sites_*.json&lt;/code&gt; etc.) at once. &lt;code&gt;core/file_lock.py::FileLock&lt;/code&gt; prevents that collision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_try_acquire_once&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FileLockTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to acquire lock within &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poll_interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The defaults are &lt;code&gt;timeout=10.0&lt;/code&gt; and &lt;code&gt;poll_interval=0.1&lt;/code&gt; — it polls every 0.1 seconds until either the lock is acquired or ten seconds pass, with no growth in the interval at all.&lt;/p&gt;

&lt;p&gt;Here again, exponential backoff wouldn't fit the actual contention pattern. This lock is contended by, at most, two processes belonging to the same app — not an unbounded number of independent clients — so there's no thundering herd to prevent in the first place. And since the wait is local disk I/O rather than load on a remote server, the cost of polling frequently is just a bit of wasted CPU wake-ups, not risk to a shared resource. Given that, polling at a short fixed interval means the lock gets picked up close to the moment it's released; a growing interval would risk sitting through an unnecessarily long gap right when the lock happens to free up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Retrying by Changing Approach, Not by Waiting
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;core/ssh_utils.py&lt;/code&gt; has a different kind of retry when fetching the plugin list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wp_with_plugins&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; plugin list --update=available --format=json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hide&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;warn&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;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="c1"&gt;# fall back: retry with all plugins skipped
&lt;/span&gt;    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wp_safe&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; plugin list --update=available --format=json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hide&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;warn&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;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first attempt runs WP-CLI without &lt;code&gt;--skip-plugins&lt;/code&gt;, so any plugin's own update-detection hook can fire. If that fails, the fallback switches immediately — with no delay at all — to a safer command that skips every plugin. This isn't the kind of failure that waiting resolves; the suspected cause is a specific plugin interfering with the command itself, a structural problem rather than a transient one. So the retry axis here isn't "how long to wait" but "which approach to use."&lt;/p&gt;

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

&lt;p&gt;Exponential backoff earns its complexity when many independent clients might hammer the same resource at once, spreading out the resulting wave of retries so an overloaded server gets room to recover — a solid default for calls to external APIs and cloud services. But when the retry count is inherently small (one HTTP recheck), when contention is limited to a couple of processes on the same machine (a local file lock), or when the failure isn't the kind that time alone fixes (switching strategy instead of retrying the same command), fixed intervals — or no delay at all — are simpler and don't leave anything on the table. The question worth asking before reaching for backoff isn't "how should the wait time grow," but "what's actually contending for what, and would waiting longer even help."&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why SSH Key Authentication Beats Password Authentication — What Is Actually Being Proven</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 24 Sep 2026 23:48:45 +0000</pubDate>
      <link>https://dev.to/susumun/why-ssh-key-authentication-beats-password-authentication-what-is-actually-being-proven-4cep</link>
      <guid>https://dev.to/susumun/why-ssh-key-authentication-beats-password-authentication-what-is-actually-being-proven-4cep</guid>
      <description>&lt;p&gt;"Use key-based auth instead of a password" is common advice for connecting to a server over SSH. But what exactly does key authentication prove, and how does that differ from what a password proves? This post works through the mechanics of what each method is actually demonstrating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication is an act of proof
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: authentication is the process of confirming that whoever just connected really is who they claim to be, based on some kind of evidence. It's often confused with authorization, which is a separate concept covering what a confirmed identity is then allowed to do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every authentication scheme ultimately comes down to a choice: what evidence counts as proof of identity? Password authentication and public-key authentication answer that question in fundamentally different ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Password authentication: proving you know a secret by stating it
&lt;/h2&gt;

&lt;p&gt;Password authentication is structurally simple. Client and server both know the same secret in advance. The client sends that secret to the server, the server compares it (usually after hashing) against its own stored record, and a match means "authenticated."&lt;/p&gt;

&lt;p&gt;The weakness lives in that structure itself: the secret gets stated, directly, as part of the exchange. SSH's transport is encrypted, so eavesdropping isn't the main concern — the problems lie elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passwords people can actually remember tend to have far lower entropy than machine-generated random values, which makes them susceptible to dictionary and brute-force attacks&lt;/li&gt;
&lt;li&gt;Reusing the same password across services turns one leak into an entry point for every other server that shares it (credential stuffing)&lt;/li&gt;
&lt;li&gt;If the value stored server-side (a hash) ever leaks, it opens the door to offline brute-forcing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, password authentication proves "I know the secret" by transmitting the secret — or a value derived directly from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public-key authentication: proving possession without ever handing over the secret
&lt;/h2&gt;

&lt;p&gt;Public-key authentication proves identity a completely different way, built on asymmetric cryptography — a public/private key pair.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: asymmetric cryptography uses two mathematically linked but distinct keys: a private key for signing or decrypting, and a public key for verifying or encrypting. Deriving one key from the other is designed to be computationally infeasible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Only the public key gets registered on the server ahead of time. The private key never leaves the client machine. During authentication, the server sends a random challenge value; the client signs it with the private key and sends the signature back. The server verifies that signature using the registered public key — a valid signature is proof that the client holds the matching private key, without the private key itself ever being transmitted.&lt;/p&gt;

&lt;p&gt;That's the decisive difference. Password authentication sends the secret itself over the wire. Public-key authentication only ever sends proof of possession (a signature), and by design, that proof can't be reverse-engineered back into the private key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens if server-side data leaks
&lt;/h2&gt;

&lt;p&gt;The difference becomes stark when you consider what a leak of server-side authentication data actually costs.&lt;/p&gt;

&lt;p&gt;With password authentication, what the server stores is a password hash. If that hash leaks, an attacker can brute-force it offline — and a weak password will eventually fall.&lt;/p&gt;

&lt;p&gt;With public-key authentication, what the server stores is the public key itself — information that's meant to be public by definition. A leak of that data costs nothing, because there was never a secret sitting on the server side to begin with. Not putting anything secret on the server is the core of why public-key authentication is safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: narrowing authentication down to one explicit key
&lt;/h2&gt;

&lt;p&gt;This app's SSH connection code (&lt;code&gt;core/ssh_utils.py::get_ssh_connection()&lt;/code&gt;) doesn't just use public-key authentication — it also deliberately narrows which key gets tried.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;connect_kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;look_for_keys&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allow_agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ssh_key_path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ssh_key_path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;pkey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_any_ssh_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ssh_passphrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;connect_kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pkey&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkey&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ssh_host&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ssh_user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ssh_port&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;connect_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connect_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connect_kwargs&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;look_for_keys=False&lt;/code&gt; and &lt;code&gt;allow_agent=False&lt;/code&gt; turn off the SSH client library's (paramiko's) default behavior of trying every key under &lt;code&gt;~/.ssh/&lt;/code&gt; and every key registered in a running SSH agent. Only the one key specified in that site's configuration is passed in explicitly via &lt;code&gt;pkey&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This isn't a matter of taste. In an environment managing many sites, leaving automatic key discovery on means a single connection attempt can end up trying several candidate keys back to back — quickly running into OpenSSH's &lt;code&gt;MaxAuthTries&lt;/code&gt; limit (6 by default). If the server side has protection like fail2ban or OpenSSH's PerSourcePenalties, a legitimate administrator can end up temporarily blocking their own IP by accident. Pinning "this connection uses exactly this one key" isn't about the strength of authentication itself — it's a structural fix for a pitfall specific to managing many sites at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning password login off entirely, server-side
&lt;/h2&gt;

&lt;p&gt;Beyond using key auth on the client, it's also standard practice to set &lt;code&gt;PasswordAuthentication no&lt;/code&gt; in &lt;code&gt;sshd_config&lt;/code&gt;, so the server won't even accept password login attempts. That's a well-documented, standard OpenSSH setting — not some private operational trick — and it follows a simple principle: eliminate the weaker authentication path rather than just discouraging its use. No matter how strong a key is, if the same account can still be reached with a password, that's where an attacker will aim.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this connects to choosing a key type
&lt;/h2&gt;

&lt;p&gt;An earlier post, &lt;a href="https://en.wpmm.jp/blog/ssh-key-types-rsa-ed25519-explained/" rel="noopener noreferrer"&gt;SSH Key Types: RSA vs. ED25519 vs. ECDSA — Which Should You Use?&lt;/a&gt;, covered which key algorithm to pick once you're using public-key authentication. This post is one layer earlier in that decision: why choose public-key authentication over a password in the first place. Picking a key algorithm is a decision that only comes up after you've already committed to that approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Password authentication proves "I know the secret" by transmitting the secret itself. Public-key authentication proves the same kind of claim — "I am who I say I am" — by transmitting only proof of possession, never the private key. That difference directly shapes the blast radius of a server-side data leak: a leaked password hash is a starting point for an attacker, while a leaked public key is worthless to one, because it was never secret to begin with. Keeping nothing secret on the server side is the most fundamental reason SSH key authentication is recommended over passwords.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Unit Tests vs. Regression Tests: Why the Same Feature Gets Tested Twice</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 24 Sep 2026 00:54:59 +0000</pubDate>
      <link>https://dev.to/susumun/unit-tests-vs-regression-tests-why-the-same-feature-gets-tested-twice-1mf6</link>
      <guid>https://dev.to/susumun/unit-tests-vs-regression-tests-why-the-same-feature-gets-tested-twice-1mf6</guid>
      <description>&lt;p&gt;Look through a maintenance tool's test suite long enough and you'll run into a small puzzle: a function already has a test, so why does another file add a second one for what looks like the same behavior? Two tests that appear to cover the same ground can actually exist for entirely different reasons.&lt;/p&gt;

&lt;p&gt;This post walks through the distinction between "unit tests" and "regression tests," using real test code from this project as the example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit tests: verifying a function in isolation
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: a unit test checks the smallest testable piece of a program — a function, class, or method — independently from the rest of the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A unit test's job is simple: feed a function a range of plausible inputs and confirm the output matches expectations. It's naturally written right alongside the implementation, or shortly after.&lt;/p&gt;

&lt;p&gt;Take the helper functions that diagnose and fix SSH private key permission issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestIsPermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_openssh_too_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Permissions 0644 for &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/Users/x/.ssh/id_rsa&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; are too open.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_perms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_permission_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_windows_openssh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Permissions on the private key file &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;Users&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;.ssh&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; are too open.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_perms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_permission_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_case_insensitive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_perms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_permission_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ARE TOO OPEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test method feeds &lt;code&gt;is_permission_error()&lt;/code&gt; a different plausible input — a typical OpenSSH message, the Windows-flavored wording, a case variation — and checks the return value. The POSIX (Mac/Linux) side runs real &lt;code&gt;chmod&lt;/code&gt; calls; the Windows &lt;code&gt;icacls&lt;/code&gt; path is mocked out since it can't run in this CI environment.&lt;/p&gt;

&lt;p&gt;The question this test is asking is: "does this function behave as designed?" It verifies a &lt;strong&gt;contract&lt;/strong&gt;, and it can be written the moment the function exists, independent of anything that happens later in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regression tests: sealing off an incident that already happened
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: "regression" means slipping backward. A regression test checks that a bug fixed in the past hasn't quietly come back with a later code change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Regression tests are a different animal. They aren't triggered by "let's verify the spec" — they're triggered by "this actually broke, in a real environment, once."&lt;/p&gt;

&lt;p&gt;Here's an incident from development on v1.6.11. A pre-build version-consistency script (&lt;code&gt;tools/bump_version.py&lt;/code&gt;) printed a success message containing an emoji (✅). On Mac, the console defaults to UTF-8, so nothing ever went wrong there. But on Windows with a Japanese locale, when this script ran as a subprocess with its output piped, Python defaulted to the locale encoding (cp932) — which can't represent that emoji — and the script crashed with a &lt;code&gt;UnicodeEncodeError&lt;/code&gt;. Worse, the caller (&lt;code&gt;build_app.py&lt;/code&gt;) misreported that crash as a "version mismatch," making the real cause harder to trace.&lt;/p&gt;

&lt;p&gt;That incident produced &lt;code&gt;tests/test_windows_cp932_safety.py&lt;/code&gt;, built as two layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 (static check)&lt;/strong&gt;: scan the source of any script that runs as a subprocess during build/release, character by character, and confirm nothing in it fails to encode as cp932.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CP932_CRITICAL_SCRIPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/bump_version.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_critical_scripts_are_fully_cp932_encodable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CP932_CRITICAL_SCRIPTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cp932&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;UnicodeEncodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# record the failure
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Layer 2 (behavioral check)&lt;/strong&gt;: actually run the target script as a child process with &lt;code&gt;PYTHONIOENCODING=cp932&lt;/code&gt; set, and confirm it exits cleanly (return code 0) without raising &lt;code&gt;UnicodeEncodeError&lt;/code&gt;. A static character scan alone can't guarantee the script won't crash at runtime, so the behavior itself gets checked separately.&lt;/p&gt;

&lt;p&gt;The question this test asks isn't "is this function correct?" — it's "&lt;strong&gt;has that specific incident happened again?&lt;/strong&gt;" The function itself was already fixed by the time this test was written, and the behavioral layer confirms the fix works. The reason this test stays in the suite indefinitely is to catch a future edit that quietly reintroduces an emoji into an output message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a "correct" function still broke
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;bump_version.py&lt;/code&gt; emoji output wasn't a logic bug in isolation — printing a checkmark on success did exactly what it was supposed to do. The problem lived in an assumption about &lt;strong&gt;which environment, and by what path, that output would be read&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A unit test verifies a function's contract in the abstract: given this input, does it return that? Looked at on its own, the emoji output passes such a test easily. The failure only surfaces when three conditions line up at once — a specific locale (cp932), output flowing through a pipe, and the call happening from inside a subprocess. No amount of exhaustively unit-testing the function's normal logic would have caught that combination.&lt;/p&gt;

&lt;p&gt;That's exactly why an incident that actually happened, across environments, gets frozen into an automated test rather than left as a lesson learned once and then forgotten.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the test filenames themselves signal
&lt;/h2&gt;

&lt;p&gt;This repository's test suite includes files like &lt;code&gt;test_stability_v47.py&lt;/code&gt; and &lt;code&gt;test_db_backup_v48.py&lt;/code&gt;, named after internal development round numbers. The header of &lt;code&gt;test_db_backup_v48.py&lt;/code&gt; reads, in part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Issue #299 (fixed 2026-04-24): regression guard for the
os.path.join(local_site_backup_dir, backup_file) bug — backup_file had
become a remote absolute path, so the second argument was treated as
absolute and the first argument was discarded entirely.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;os.path.join()&lt;/code&gt; has a documented behavior: if a later argument is an absolute path, everything before it is discarded and only that absolute path is returned. That's not a bug — it's the documented contract. The actual incident was that the calling code assumed the string it passed in would always be a relative filename, when in practice a remote absolute path made it through, silently discarding the intended local backup directory.&lt;/p&gt;

&lt;p&gt;A test carrying an issue number or round number in its filename or header comment is a signal that it exists to guard against a &lt;strong&gt;specific past incident&lt;/strong&gt;, not to exercise the general behavior of a module. By contrast, a file named purely after the module it covers — like &lt;code&gt;test_key_perms.py&lt;/code&gt; — is a unit test aimed at verifying that module's spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connection to visual regression testing
&lt;/h2&gt;

&lt;p&gt;An earlier post on this blog, &lt;a href="https://en.wpmm.jp/blog/visual-regression-testing-screenshot-diff-basics/" rel="noopener noreferrer"&gt;What Is Visual Regression Testing? How Screenshot Diffing Catches Layout Breaks&lt;/a&gt;, covers a variant of this same idea. That post froze "has the layout broken" into a pixel-comparison check; the cp932 example here freezes "has a specific piece of logic broken under a specific environment" into a code-execution check. The verification mechanism differs — image diffing versus running a script — but the underlying design is the same: take an incident that already happened once, and turn it into something that can be re-checked automatically, indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Unit tests confirm that a function or module behaves according to spec, written around the same time as the implementation. Regression tests exist to make sure an incident that already happened once, in a real environment, doesn't quietly happen again after a future code change — and they're written after the fact. Both take the shape of "a test," but they're born at different moments and serve different purposes. When a test suite has multiple files touching what looks like the same feature, it's often carrying two distinct kinds of intent at once: verifying a spec, and remembering an incident.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Desktop Apps Detect and Kill Stale Processes on Startup</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 22 Sep 2026 01:16:42 +0000</pubDate>
      <link>https://dev.to/susumun/how-desktop-apps-detect-and-kill-stale-processes-on-startup-209o</link>
      <guid>https://dev.to/susumun/how-desktop-apps-detect-and-kill-stale-processes-on-startup-209o</guid>
      <description>&lt;p&gt;You close a desktop app, but its process is still sitting there in the task manager or Activity Monitor. Many people have run into this. This article looks at the design behind a common fix: detecting a leftover process from a previous run at startup, cleaning it up safely, and only then starting fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Process Can Fail to Exit
&lt;/h2&gt;

&lt;p&gt;A Python desktop app built with a Flask backend and a browser as its display, packaged into a single executable with PyInstaller, often relies on a hard-exit call like &lt;code&gt;os._exit(0)&lt;/code&gt; to shut down. The catch: calling this from a background daemon thread doesn't always terminate the process in a frozen (packaged) build. From the user's point of view, they clicked "quit," but the process kept running behind the scenes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: a daemon thread is one that gets forcibly terminated when the main thread exits. It's commonly used for background work, but calling &lt;code&gt;os._exit()&lt;/code&gt; from inside one doesn't guarantee the whole process actually terminates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A leftover process like this causes trouble the next time the app is launched — the port it was using is still occupied, or two instances end up running and conflicting with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separating "the PID is alive" from "the port is responding"
&lt;/h2&gt;

&lt;p&gt;This app writes the running instance's information to a port file (&lt;code&gt;app_running.port&lt;/code&gt;) as &lt;code&gt;port\nPID&lt;/code&gt;. On the next launch, it reads this file and checks whether the recorded PID is actually still alive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_pid_alive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check whether a PID is alive (macOS/Windows)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;win32&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;
            &lt;span class="n"&gt;kernel32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kernel32&lt;/span&gt;
            &lt;span class="n"&gt;SYNCHRONIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x00100000&lt;/span&gt;
            &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kernel32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OpenProcess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SYNCHRONIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;kernel32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CloseHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ProcessLookupError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's worth noting here is that checking whether a process is alive works completely differently on Unix-like systems versus Windows. On Unix-like systems, &lt;code&gt;os.kill(pid, 0)&lt;/code&gt; sends signal number 0 — which, per the POSIX spec, doesn't actually deliver a signal at all. It's a special case that only performs permission and existence checks. A nonexistent PID raises &lt;code&gt;ProcessLookupError&lt;/code&gt; (a subclass of &lt;code&gt;OSError&lt;/code&gt;). Windows has no equivalent to signal 0, so the check instead tries to obtain a process handle via &lt;code&gt;OpenProcess()&lt;/code&gt; and treats success or failure as the liveness result.&lt;/p&gt;

&lt;p&gt;But "the PID is alive" and "the process is working correctly" aren't the same thing, and that distinction is the core of this design. A process can be alive while its main loop is hung or its listening socket has stopped responding — from the outside, that's indistinguishable from an unresponsive zombie. So beyond checking whether the PID exists, the code also checks whether the recorded port actually accepts a connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_kill_stale_process&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_read_port_file&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;_is_pid_alive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Process already exited, only the port file is left behind — clean up only
&lt;/span&gt;        &lt;span class="nf"&gt;_cleanup_instance_files&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="c1"&gt;# Process is alive but the port doesn't respond — treat it as a zombie
&lt;/span&gt;    &lt;span class="n"&gt;is_listening&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOCK_STREAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;settimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;is_listening&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_ex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_listening&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;_force_kill_pid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;_cleanup_instance_files&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# if is_listening=True (running normally), leave the port file alone
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function splits the port file's state into three cases at startup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The PID is already dead&lt;/strong&gt;: the process already exited, cleanly or otherwise. Only the leftover port file needs cleaning up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PID is alive but the port doesn't respond&lt;/strong&gt;: the process is still around but isn't doing its job — a zombie. This is the case that gets force-killed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PID is alive and the port responds&lt;/strong&gt;: a genuinely running instance. Nothing happens here, and the port file is left untouched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Misclassifying case 3 as "an old process" and killing it would take down a healthy instance the user never asked to stop. Not stopping at a liveness check, and going one step further to confirm the process is actually serving requests, is a deliberate second layer meant to avoid exactly that kind of false positive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Force-killing also differs by OS
&lt;/h2&gt;

&lt;p&gt;Just like the liveness check, the mechanism for force-killing a process differs by platform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_force_kill_pid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Force-kill a PID (macOS/Windows)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;win32&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;taskkill&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/F&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/PID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
                &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;creationflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CREATE_NO_WINDOW&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Unix-like systems, this sends &lt;code&gt;SIGKILL&lt;/code&gt;. That signal can't be caught or ignored by the target process — it's a kernel-level termination, which is exactly what's needed for a zombie process whose normal signal handlers may not even be functioning anymore. Windows has no direct equivalent, so the code shells out to &lt;code&gt;taskkill /F&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  macOS has its own reactivation quirk
&lt;/h2&gt;

&lt;p&gt;Everything above is about cleaning up after a process that failed to exit properly. macOS adds a separate wrinkle on top of that: clicking an already-running app's icon in the Dock or Finder doesn't launch a new process at all — it just "activates" (brings to the front) the existing one. That's standard macOS behavior across the board, but it's not automatically convenient for an app built around a Flask backend plus a browser front end.&lt;/p&gt;

&lt;p&gt;In a frozen build running on macOS, when this app detects an existing instance already running, it requests a graceful shutdown (waiting for any maintenance operation in progress to finish), sends &lt;code&gt;SIGTERM&lt;/code&gt;, waits up to 15 seconds, and falls back to &lt;code&gt;SIGKILL&lt;/code&gt; if the process is still alive — only then does it start up again as a new process. Without this, simply reactivating the existing instance wouldn't reflect what the user actually wants when they double-click the app expecting a fresh launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different problem: the browser tab closes, the server doesn't
&lt;/h2&gt;

&lt;p&gt;Separate from detecting and killing stale processes at startup, this app also handles a different kind of leftover-process problem: what happens when the browser tab is closed but the server process keeps running. The browser sends a request to &lt;code&gt;/api/heartbeat&lt;/code&gt; every 30 seconds, and if the server hasn't received one in 60 seconds — and no maintenance operation is in progress — it sends itself &lt;code&gt;SIGKILL&lt;/code&gt; and exits.&lt;/p&gt;

&lt;p&gt;This mechanism serves a different purpose and fires at a different time than the startup-time stale-process check. Heartbeat monitoring answers "how does a running process realize it's been abandoned by the user," while &lt;code&gt;_kill_stale_process&lt;/code&gt; answers "how does a newly starting process clean up the wreckage of a previous one." They can look like the same category of problem — process cleanup — but they differ in who's doing the detecting (the running process itself, versus the next process about to start) and when it happens (continuous monitoring, versus a one-time check at launch).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Point&lt;/th&gt;
&lt;th&gt;Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checking if a PID is alive&lt;/td&gt;
&lt;td&gt;Unix-like systems use &lt;code&gt;os.kill(pid, 0)&lt;/code&gt;; Windows uses &lt;code&gt;OpenProcess()&lt;/code&gt; — the mechanism differs by OS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Alive" vs. "actually working"&lt;/td&gt;
&lt;td&gt;Checking whether the port responds, not just whether the PID exists, avoids killing a healthy instance by mistake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force-killing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SIGKILL&lt;/code&gt; on Unix-like systems, &lt;code&gt;taskkill /F&lt;/code&gt; on Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS reactivation&lt;/td&gt;
&lt;td&gt;Because Dock/Finder won't launch a new process for a running app, the old one has to be explicitly terminated before starting fresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vs. heartbeat monitoring&lt;/td&gt;
&lt;td&gt;Startup cleanup and abandonment detection solve different problems on different timelines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Something as seemingly simple as "terminate a process" turns out to involve several stacked decisions: platform-specific ways to check whether something is alive, a multi-step check to avoid killing a healthy process by accident, and handling for platform-specific quirks like macOS reactivation.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Semantic Versioning (SemVer): Why Version Numbers Have Three Parts</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sun, 20 Sep 2026 23:40:15 +0000</pubDate>
      <link>https://dev.to/susumun/semantic-versioning-semver-why-version-numbers-have-three-parts-1f74</link>
      <guid>https://dev.to/susumun/semantic-versioning-semver-why-version-numbers-have-three-parts-1f74</guid>
      <description>&lt;p&gt;Most software version numbers look like &lt;code&gt;1.6.11&lt;/code&gt; — three numbers separated by dots. This isn't an arbitrary naming choice; it follows a widely adopted convention called Semantic Versioning, or SemVer. This article looks at why version numbers are split into three parts, and what it actually takes to implement that convention correctly in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MAJOR.MINOR.PATCH Each Mean
&lt;/h2&gt;

&lt;p&gt;SemVer formats a version as &lt;code&gt;MAJOR.MINOR.PATCH&lt;/code&gt; (for example, &lt;code&gt;1.6.11&lt;/code&gt;), and each position carries a distinct meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MAJOR&lt;/strong&gt;: incremented when you make a breaking change — something that could stop existing usage from working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MINOR&lt;/strong&gt;: incremented when you add functionality in a backward-compatible way — existing usage keeps working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PATCH&lt;/strong&gt;: incremented when you fix a bug in a backward-compatible way — no behavior-breaking side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: "backward-compatible" means code or usage written for an older version keeps working on the newer one. A breaking change is one where something that used to work stops working after the update.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What matters here is that these three numbers aren't just a sequential counter — each position is assigned a specific meaning. Just by glancing at a version number, you can get a rough sense of whether an update is likely safe to apply immediately or whether it deserves a closer look first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Single Incrementing Number Isn't Enough
&lt;/h2&gt;

&lt;p&gt;Imagine version numbers were just a plain sequence: v1, v2, v3, and so on. Looking at a single number like that tells you nothing about whether the change behind it was a minor fix or a major overhaul. Users would have to read the release notes every single time just to figure out whether it's safe to upgrade — the number itself carries almost no information.&lt;/p&gt;

&lt;p&gt;Splitting the version into MAJOR.MINOR.PATCH is a way to make the number itself carry meaning. If only the PATCH digit moved, you can generally trust it's safe to apply right away. If MAJOR moved, that's a signal to check what changed before upgrading. The structure of the number becomes a form of communication in itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Version Comparisons Need to Be Numeric
&lt;/h2&gt;

&lt;p&gt;This app stores its current version as &lt;code&gt;VERSION = "1.6.11"&lt;/code&gt; in &lt;code&gt;version.py&lt;/code&gt;, and compares it against the latest version published on the update server at startup to decide whether an update is available. This comparison step hides a classic pitfall that's easy to overlook when working with SemVer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_newer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remote_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_version&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compare versions using semantic versioning (X.Y.Z)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;remote_version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;current_version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;AttributeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this comparison were done as a plain string comparison (&lt;code&gt;"1.10.0" &amp;gt; "1.9.0"&lt;/code&gt;), the result would actually come out wrong. Python's string comparison walks character by character in lexicographic order, so &lt;code&gt;"1.10.0"&lt;/code&gt; ends up evaluated as &lt;strong&gt;smaller&lt;/strong&gt; than &lt;code&gt;"1.9.0"&lt;/code&gt; — the first character &lt;code&gt;1&lt;/code&gt; matches, but at the second character it's comparing &lt;code&gt;.&lt;/code&gt; against &lt;code&gt;.&lt;/code&gt;, then &lt;code&gt;1&lt;/code&gt; against &lt;code&gt;9&lt;/code&gt;, and &lt;code&gt;1&lt;/code&gt; sorts before &lt;code&gt;9&lt;/code&gt;. Numerically, &lt;code&gt;1.10.0&lt;/code&gt; is the newer version, but a naive string comparison gets it backwards.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_is_newer&lt;/code&gt; avoids this by splitting each version string on the dot, converting each segment to an integer with &lt;code&gt;int()&lt;/code&gt;, and comparing the resulting tuples. Tuple comparison in Python evaluates elements left to right as numbers, so &lt;code&gt;(1, 10, 0) &amp;gt; (1, 9, 0)&lt;/code&gt; correctly evaluates to &lt;code&gt;True&lt;/code&gt;. Only by treating each of the three parts as an independent number — rather than as characters in a string — does the version ordering actually match its intended meaning. The visual ordering of the string and the semantic ordering of "which version is newer" are not always the same thing, and that distinction is the whole point of this design.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Version Number Doesn't Live in Just One Place
&lt;/h2&gt;

&lt;p&gt;There's a second practical challenge in applying SemVer in a real project: the version string rarely lives in just one file. It typically ends up duplicated across the installer configuration, a metadata file on the distribution server, and filenames referenced in download links. In this app, beyond &lt;code&gt;version.py&lt;/code&gt; itself, the version string is also embedded separately in the installer-generation config, a distribution metadata file, and the download page's link text.&lt;/p&gt;

&lt;p&gt;Keeping all of these in sync by hand is a standing risk — it's easy to forget one. This project actually hit that exact problem once: the installer configuration was left un-bumped while every other file moved forward, so the installer that got built still carried the old version number. To close that gap, we built a script that lists every file that needs updating and bumps them all together, plus a &lt;code&gt;--check&lt;/code&gt; mode that verifies every file's embedded version string actually matches the single source of truth in &lt;code&gt;version.py&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;python tools/bump_version.py 1.6.11 2026-06-11   &lt;span class="c"&gt;# bump every target file at once&lt;/span&gt;
python tools/bump_version.py &lt;span class="nt"&gt;--check&lt;/span&gt;              &lt;span class="c"&gt;# verify consistency only (exit 1 on mismatch)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--check&lt;/code&gt; is wired in as a gate before the build step runs. If any file's version string drifts from what &lt;code&gt;version.py&lt;/code&gt; declares, the build stops right there. Rather than relying on someone remembering to check every file by hand, the consistency check runs mechanically, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Point&lt;/th&gt;
&lt;th&gt;Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MAJOR.MINOR.PATCH split&lt;/td&gt;
&lt;td&gt;Each digit encodes whether a change is breaking, additive, or a plain fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vs. a plain counter&lt;/td&gt;
&lt;td&gt;A single incrementing number carries almost no information on its own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The string comparison trap&lt;/td&gt;
&lt;td&gt;Lexicographic comparison gets &lt;code&gt;"1.10.0" &amp;lt; "1.9.0"&lt;/code&gt; wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparing as tuples&lt;/td&gt;
&lt;td&gt;Converting each segment to &lt;code&gt;int()&lt;/code&gt; and comparing as tuples restores correct ordering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version numbers, duplicated&lt;/td&gt;
&lt;td&gt;Version strings scattered across multiple files need a bump-and-verify script to avoid drift&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Semantic Versioning isn't just a naming convention — it's a design choice to make the version number itself a communication tool for whoever is about to upgrade. Actually implementing that idea correctly in code means paying attention to details outside the convention itself, like numeric comparison correctness and keeping duplicated version strings in sync.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Symmetric Encryption (Fernet) Keeps Local Credentials Safe on Disk</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sat, 19 Sep 2026 05:16:23 +0000</pubDate>
      <link>https://dev.to/susumun/how-symmetric-encryption-fernet-keeps-local-credentials-safe-on-disk-38bk</link>
      <guid>https://dev.to/susumun/how-symmetric-encryption-fernet-keeps-local-credentials-safe-on-disk-38bk</guid>
      <description>&lt;p&gt;Desktop apps that talk to servers over SSH or an API often need to remember a password or key between launches. Asking the user to retype it every time isn't realistic, but saving it as plain text in a config file is risky — the moment that file ends up in a backup, a sync folder, or gets shared with someone for debugging, the credential is exposed. This post looks at how symmetric encryption solves that specific problem, using Python's &lt;code&gt;cryptography&lt;/code&gt; library and its &lt;code&gt;Fernet&lt;/code&gt; recipe as a concrete example.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Symmetric encryption uses the same key for both encrypting and decrypting. That's different from the SSH keys covered in &lt;a href="https://en.wpmm.jp/blog/ssh-key-types-rsa-ed25519-explained/" rel="noopener noreferrer"&gt;SSH key types (RSA / ED25519 / ECDSA) — what actually differs, and which one to pick&lt;/a&gt;, which are asymmetric — a public/private key pair. Asymmetric encryption exists to let two separate parties authenticate or communicate without ever sharing a secret. Symmetric encryption fits a different case: a single program encrypting data now so that the same program can read it back later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why symmetric encryption is the right tool here
&lt;/h2&gt;

&lt;p&gt;SSH connections and API calls involve two separate parties — the local machine and a remote server — so a secure way to exchange keys matters, and that's exactly what asymmetric cryptography is built for. Local credential storage is a different problem: the app encrypts its own configuration and later decrypts it for its own use. There's no second party to negotiate a key with. A single key, kept somewhere safe, is enough. Symmetric algorithms are also computationally lighter, which suits this "lock your own box with your own key" use case well.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Fernet actually bundles together
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Fernet&lt;/code&gt;, part of Python's &lt;code&gt;cryptography&lt;/code&gt; library, packages a set of well-understood primitives into one safe-to-use recipe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AES (Advanced Encryption Standard)&lt;/strong&gt; encrypts the payload itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HMAC (Hash-based Message Authentication Code)&lt;/strong&gt; signs the ciphertext so tampering can be detected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An embedded timestamp&lt;/strong&gt; records when the token was created, which can later support expiration checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL-safe Base64 encoding&lt;/strong&gt; turns the whole thing into a printable string that drops cleanly into JSON or a text file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is that Fernet handles authentication, not just confidentiality. Plain encryption without an integrity check can still be tampered with — an attacker without the key generally can't read the plaintext, but might still be able to flip bits in the ciphertext and corrupt it in a way that goes unnoticed. Fernet's built-in HMAC check catches that: if a token has been altered since it was created, decryption fails outright.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.fernet&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Fernet&lt;/span&gt;

&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# 32 random bytes, URL-safe base64 encoded
&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fernet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-secret-password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# encrypt -&amp;gt; token
&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# decrypt -&amp;gt; original bytes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where the key lives, and how not to lose it
&lt;/h2&gt;

&lt;p&gt;The hardest part of using symmetric encryption in practice usually isn't the algorithm — it's key management. Anyone holding the key can decrypt the data, and losing the key makes the encrypted data permanently unreadable. That's a fundamentally different failure mode than a forgotten password, which can just be reset, so persisting the key safely deserves real care.&lt;/p&gt;

&lt;p&gt;This app generates a key the first time it runs, and stores it in a hidden file under the user's home directory with permissions restricted to the file's owner. On later launches it reads back the same key. As a safeguard against that file disappearing — accidental deletion, a corrupted profile directory — a second copy of the same key is kept in another location, and at startup both locations are checked so that a missing copy can be restored from whichever one still exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified: try the primary location, fall back to the backup,
# and re-sync whichever copy is missing.
&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRIMARY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;load_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BACKUP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;Fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;sync_to_both_locations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the key in more than one place and reconciling them on every startup is unglamorous compared to the encryption itself, but it matters just as much in practice. A single storage location turns any accidental deletion or disk hiccup directly into unrecoverable data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting a value announce its own encryption state
&lt;/h2&gt;

&lt;p&gt;One more detail worth noting: every encrypted value in this app is prefixed with &lt;code&gt;ENC:&lt;/code&gt; before being saved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encrypt_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENC:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;  &lt;span class="c1"&gt;# already encrypted, skip
&lt;/span&gt;    &lt;span class="n"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENC:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;encrypted&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prefix solves two practical problems at once. First, it makes encryption idempotent — calling it twice on an already-encrypted value won't double-encrypt it. Second, it lets old plaintext data and newly encrypted data coexist in the same file without ambiguity: the value itself tells you which state it's in. That means encryption support can be added to an existing app without a one-time migration pass — new writes get encrypted immediately, and old entries only get encrypted the next time they're written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encrypting fields by allow-list, not by default
&lt;/h2&gt;

&lt;p&gt;Rather than encrypting an entire configuration blob, this app exposes a helper that takes an explicit list of keys to encrypt within a dictionary — the caller decides what counts as sensitive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encrypt_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys_to_encrypt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys_to_encrypt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="c1"&gt;# Only the genuinely sensitive fields are named explicitly
&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ssh_password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Encrypting non-sensitive fields like a site name or URL would make the config file unreadable and hard to diff by hand for no real benefit. An explicit allow-list keeps the sensitive fields protected while leaving everything else plain and inspectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failing soft when decryption goes wrong
&lt;/h2&gt;

&lt;p&gt;Finally, decryption here doesn't raise on failure — it just returns the value unchanged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decrypt_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;  &lt;span class="c1"&gt;# return as-is rather than raising
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That choice matters if a config file ever gets loaded with the wrong key (say, one generated on a different machine) or gets corrupted by a manual edit. Instead of crashing the whole app, one unreadable field is returned untouched while everything else keeps loading normally. A degraded state — one field still garbled — is a better outcome than the app failing to start at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design piece&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fernet (AES + HMAC + timestamp)&lt;/td&gt;
&lt;td&gt;Encryption with built-in tamper detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key stored in two locations with mutual recovery&lt;/td&gt;
&lt;td&gt;Reduces the risk that losing the key means losing the data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ENC:&lt;/code&gt; prefix&lt;/td&gt;
&lt;td&gt;Prevents double-encryption; distinguishes old plaintext from new ciphertext&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allow-list-based &lt;code&gt;encrypt_dict&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Encrypts only sensitive fields, keeps the rest readable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decryption fails soft, not hard&lt;/td&gt;
&lt;td&gt;One corrupted field doesn't take down the whole app&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Symmetric encryption itself is a well-established, boring technology. What actually determines whether it works safely in a real app is everything around it — where the key lives and how it survives being lost, how encrypted and plaintext values are told apart, and what happens when decryption doesn't go as planned.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>What `rsync -avz --delete` Actually Does</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:13:53 +0000</pubDate>
      <link>https://dev.to/susumun/what-rsync-avz-delete-actually-does-3hnl</link>
      <guid>https://dev.to/susumun/what-rsync-avz-delete-actually-does-3hnl</guid>
      <description>&lt;p&gt;A lot of people paste &lt;code&gt;rsync -avz --delete&lt;/code&gt; into a deploy script and move on without ever unpacking what each letter means. It works, so there's rarely a reason to stop and ask. But understanding what each flag actually instructs the command to do makes it much easier to reason about exactly how far &lt;code&gt;--delete&lt;/code&gt; reaches, and why the other flags are there in the first place. This post breaks down the main options and shows how they're actually used in a real theme deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: rsync ("remote sync") is a file-synchronization tool that compares source and destination and transfers only what has changed. Unlike a plain copy, it uses a delta-transfer algorithm internally, which is where it earns its keep when the same files get synced over and over.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;archive mode&lt;/td&gt;
&lt;td&gt;Recursively copies files while preserving permissions, timestamps, symlinks, and ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;verbose&lt;/td&gt;
&lt;td&gt;Prints the name of every file transferred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;compress&lt;/td&gt;
&lt;td&gt;Compresses data in transit to reduce bandwidth usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;delete sync&lt;/td&gt;
&lt;td&gt;Removes files from the destination that no longer exist in the source, making the destination an exact mirror&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;remote shell&lt;/td&gt;
&lt;td&gt;Specifies how to connect (which SSH key, port, and options to use)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;-a&lt;/code&gt; isn't one flag — it's a bundle of flags
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;-a&lt;/code&gt; looks like a single option, but it's shorthand for several flags combined. Expanded, it's equivalent to &lt;code&gt;-rlptgoD&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Expanded&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;recursive — process directories recursively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;copy symlinks as symlinks, not as the files they point to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;preserve permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;preserve timestamps (modification time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;preserve group ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;preserve owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;preserve device and special files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one that's easy to overlook is &lt;code&gt;-t&lt;/code&gt; (preserve timestamps). rsync decides whether a file needs re-transferring by comparing file size and modification time against what it saw last time. If timestamps got reset to "now" on every sync instead of being preserved, rsync would see a different modification time on every run even when the file's content hadn't changed at all — and it would fall back to re-transferring everything, which defeats the point of a delta-transfer tool. Using &lt;code&gt;-a&lt;/code&gt; isn't just about keeping metadata intact; it's what lets rsync's own change-detection logic work correctly in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How far does &lt;code&gt;--delete&lt;/code&gt; actually reach?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--delete&lt;/code&gt; makes the destination match the source exactly: anything removed from the source also gets removed from the destination. The part worth understanding precisely is that &lt;strong&gt;it only operates within the specific source and destination directories you point it at.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you run &lt;code&gt;rsync -avz --delete "$SRC" "$DEST"&lt;/code&gt; against a theme-specific directory, &lt;code&gt;--delete&lt;/code&gt; only affects whatever lives under &lt;code&gt;$DEST&lt;/code&gt; — it has no effect on sibling directories outside that path (for example, other themes sitting alongside it under a shared &lt;code&gt;themes/&lt;/code&gt; folder). In practice, the blast radius of &lt;code&gt;--delete&lt;/code&gt; is entirely determined by how narrowly you scope the destination path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: this project's theme deployment command
&lt;/h2&gt;

&lt;p&gt;Deploying this blog's theme (&lt;code&gt;wpmm-blog&lt;/code&gt;) to the server actually uses this command:&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="nv"&gt;SRC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;server/wpmm-blog-theme/wpmm-blog/
&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;layer2024@layer2024.xsrv.jp:wpmm.jp/public_html/blog/wp-content/themes/wpmm-blog/

rsync &lt;span class="nt"&gt;-avz&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"ssh -i ~/.ssh/layer2024_xserver.key -p 10022 -o BatchMode=yes"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;--delete&lt;/code&gt; is scoped to the theme's own directory (&lt;code&gt;wp-content/themes/wpmm-blog/&lt;/code&gt;), so the only files it can actually remove are old theme files that have been deleted from the local source but are still lingering on the server. If the destination were mistakenly pointed at &lt;code&gt;wp-content/themes/&lt;/code&gt; (the entire themes folder) instead, other themes — including WordPress's own bundled default themes — would be exposed to deletion too. Scoping the destination down to exactly the directory you intend to deploy is what keeps &lt;code&gt;--delete&lt;/code&gt; safe to use in practice.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-e&lt;/code&gt; flag bundles the SSH key file, port number, and &lt;code&gt;BatchMode=yes&lt;/code&gt; (which makes SSH fail immediately instead of waiting at an interactive password prompt) into a single connection specification. That matters specifically for unattended scripts — without it, a script running with no one watching could hang indefinitely at a prompt no one is there to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trailing slash changes the result
&lt;/h2&gt;

&lt;p&gt;rsync has a well-known quirk: whether or not the source path ends in a slash (&lt;code&gt;/&lt;/code&gt;) changes what actually gets copied.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rsync -av /path/to/src/ /path/to/dest/&lt;/code&gt; — copies the &lt;strong&gt;contents&lt;/strong&gt; of &lt;code&gt;src&lt;/code&gt; directly into &lt;code&gt;dest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rsync -av /path/to/src /path/to/dest/&lt;/code&gt; — copies the &lt;code&gt;src&lt;/code&gt; directory &lt;strong&gt;itself&lt;/strong&gt;, landing as &lt;code&gt;dest/src&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the theme deployment example above, &lt;code&gt;SRC=server/wpmm-blog-theme/wpmm-blog/&lt;/code&gt; ends in a slash, so the contents of &lt;code&gt;wpmm-blog/&lt;/code&gt; land directly under the destination's &lt;code&gt;wpmm-blog/&lt;/code&gt; directory rather than nesting one level deeper. Building a command without paying attention to the trailing slash is an easy way to end up with files one directory level off from where you meant them to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;In one line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recursive copy that preserves file attributes (bundles &lt;code&gt;-rlptgoD&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prints what got transferred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compresses data to save bandwidth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mirrors the destination to the source — scoping the destination path is the key to using it safely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specifies the connection method (SSH key, port, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;-avz --delete&lt;/code&gt; is a common combination, but each character carries its own distinct responsibility. &lt;code&gt;--delete&lt;/code&gt; in particular only reaches as far as the destination path you give it, so being deliberate about that scope is the basic safeguard against deleting more than you intended.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>HTTP Status Code Basics: What 200/301/403/500 Actually Mean for Maintenance Tools</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 17 Sep 2026 00:18:16 +0000</pubDate>
      <link>https://dev.to/susumun/http-status-code-basics-what-200301403500-actually-mean-for-maintenance-tools-424a</link>
      <guid>https://dev.to/susumun/http-status-code-basics-what-200301403500-actually-mean-for-maintenance-tools-424a</guid>
      <description>&lt;p&gt;WordPress site maintenance constantly involves one basic question: after an update, is the site still working? Instead of relying on a human eyeballing a page and deciding "looks fine," most tooling answers that question mechanically, using the three-digit HTTP status code a web server returns for every request. That single number carries more information than it looks like at first glance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: An HTTP status code is a three-digit number a web server always attaches to its response to a client (a browser or a program). It comes paired with a short phrase describing what it means, like &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The leading digit sets the broad category
&lt;/h2&gt;

&lt;p&gt;The hundreds digit of a status code determines its broad meaning.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1xx&lt;/td&gt;
&lt;td&gt;Informational&lt;/td&gt;
&lt;td&gt;Request received, processing continues (rarely relevant in day-to-day work)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;The request was handled successfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3xx&lt;/td&gt;
&lt;td&gt;Redirection&lt;/td&gt;
&lt;td&gt;The client needs to be pointed somewhere else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4xx&lt;/td&gt;
&lt;td&gt;Client error&lt;/td&gt;
&lt;td&gt;Something is wrong on the requesting side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;Server error&lt;/td&gt;
&lt;td&gt;Something failed on the server side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even this broad grouping alone enables a first-pass judgment: "2xx generally means ignore it," "5xx always means investigate." Automated monitoring in maintenance tooling starts from exactly this coarse filter before looking any closer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codes you actually run into during maintenance work
&lt;/h2&gt;

&lt;p&gt;Within those broad categories, a handful of specific codes show up constantly in maintenance work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200 OK&lt;/strong&gt; — the request succeeded and normal content was returned. The baseline "everything is fine" state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;301 Moved Permanently&lt;/strong&gt; — the requested URL has permanently moved to a different URL. Browsers automatically follow this to the new location. In WordPress this shows up when unifying &lt;code&gt;http&lt;/code&gt; to &lt;code&gt;https&lt;/code&gt;, or after a URL structure change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;403 Forbidden&lt;/strong&gt; — the server understood the request but is explicitly refusing to grant access. This isn't a login prompt; it's a deliberate "you may not see this." It typically shows up on sites protected by IP restrictions or Basic Auth when accessed unexpectedly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404 Not Found&lt;/strong&gt; — the requested page doesn't exist. A classic case is following an old link to a post whose URL has since changed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500 Internal Server Error&lt;/strong&gt; — the server hit an unexpected failure while processing the request. This is frequently how a PHP Fatal Error (a syntax error, a call to an undefined function, exhausted memory) surfaces externally, and it's one of the codes WordPress maintenance work watches for most closely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers come from an RFC (a published internet technical standard), so they mean the same thing regardless of which server software or programming language produced the response — a genuinely shared vocabulary across the entire web.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sixth status: "0"
&lt;/h2&gt;

&lt;p&gt;In practical monitoring code, you'll often see a special value alongside the standard 1xx–5xx range: 0. This isn't part of the HTTP specification — it's a convention monitoring scripts use to represent &lt;strong&gt;no response came back at all&lt;/strong&gt;. DNS resolution failure, connection timeout, an SSL certificate error — anything that fails before the request even reaches the server lands here.&lt;/p&gt;

&lt;p&gt;A server explicitly saying "500" and a server saying nothing at all point to very different root causes, so keeping these two cases distinct matters for how monitoring logic is designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: deciding whether to roll back by comparing before and after
&lt;/h2&gt;

&lt;p&gt;This app updates WordPress core and plugins one item at a time, checking the HTTP status right after each individual update to decide whether things got worse — and if so, rolling back only that one update. The decision logic lives in a function called &lt;code&gt;_should_rollback()&lt;/code&gt; in &lt;code&gt;maintenance_agent.py&lt;/code&gt;, and it can be simplified to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_should_rollback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev_status&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prev_status&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

    &lt;span class="c1"&gt;# Server-level failure always triggers a rollback
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="c1"&gt;# 4xx regression: only fires if the previous state was below 4xx
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The categories above map directly onto this decision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;500-range or 0 (no response) always triggers a rollback&lt;/strong&gt;, regardless of what came before. That's a server-level failure, and it's an unambiguous regression no matter what the prior state was&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400-range only triggers a rollback if the previous state was below 400.&lt;/strong&gt; This isn't a blanket "any 4xx means fail" rule, and there's a reason: some maintained sites deliberately sit behind Basic Auth (401) or IP restrictions (403) — staging environments, for instance. If the check simply flagged any post-update 4xx as a failure, a site that already returned 401 before the update would trigger a false rollback on every single run, even though nothing actually changed. By judging "did this get worse compared to right before," a site's own deliberate baseline state gets correctly treated as normal, not as a problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The function that supplies &lt;code&gt;post_status&lt;/code&gt; — &lt;code&gt;_http_status_check_stable()&lt;/code&gt; — adds one more layer: it only retries, once, after a short delay, when the status comes back as 0 (no response). The intent is to avoid misreading a brief network blip or a momentary TLS handshake failure as the site being down, which would otherwise trigger an unnecessary rollback. A 500-range response, by contrast, isn't retried — it's treated as a real error immediately. The distinction is between "an uncertain state where we genuinely don't know yet" and "the server is explicitly telling us something is wrong," and the code treats those two situations differently on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for maintenance tooling
&lt;/h2&gt;

&lt;p&gt;Reading this three-digit number mechanically, instead of having a human open every page in a browser to eyeball it, is what makes it possible to check a large number of pages across a large number of sites against a consistent standard. Because status codes are a shared, unambiguous vocabulary, it becomes possible to verify "did this update break anything?" after every single change — without depending on human attention, and while leaving a clear trail of what was checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Typical handling in maintenance work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Normal — generally safe to ignore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3xx&lt;/td&gt;
&lt;td&gt;Redirection&lt;/td&gt;
&lt;td&gt;Verify the move was intentional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4xx&lt;/td&gt;
&lt;td&gt;Client error&lt;/td&gt;
&lt;td&gt;Compare against the prior state before treating it as a regression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;Server error&lt;/td&gt;
&lt;td&gt;Always a real problem — investigate or roll back first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0 (convention)&lt;/td&gt;
&lt;td&gt;No response&lt;/td&gt;
&lt;td&gt;A connection-stage failure — retry once to rule out a transient blip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once you have both the broad category behind the three-digit number and the habit of reading it as "how did this change compared to right before" rather than in isolation, monitoring design gets noticeably more accurate.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Reading `.htaccess` mod_rewrite Rules: A Real `RewriteBase` Trap From a Subdomain Migration</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 00:28:26 +0000</pubDate>
      <link>https://dev.to/susumun/reading-htaccess-modrewrite-rules-a-real-rewritebase-trap-from-a-subdomain-migration-3j56</link>
      <guid>https://dev.to/susumun/reading-htaccess-modrewrite-rules-a-real-rewritebase-trap-from-a-subdomain-migration-3j56</guid>
      <description>&lt;p&gt;A WordPress URL like &lt;code&gt;https://example.com/blog/some-post-title/&lt;/code&gt; looks clean, but there's no directory or file on the server that actually matches that path. Behind the scenes, Apache's &lt;code&gt;mod_rewrite&lt;/code&gt; module intercepts the request and silently forwards it to &lt;code&gt;index.php&lt;/code&gt;. The rules that make this happen live in &lt;code&gt;.htaccess&lt;/code&gt;, and when a WordPress site suddenly starts throwing 404s or 500s for no obvious reason, the cause is very often a misread rule in that file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;.htaccess&lt;/code&gt; is a configuration file Apache reads on a &lt;strong&gt;per-directory&lt;/strong&gt; basis. On shared hosting environments where you can't edit the server's global configuration directly, dropping this file into a directory is often the only way to change how requests to that directory behave — which is exactly why it's one of the files people touch most often on shared hosting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Basic Syntax — Four Building Blocks
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.htaccess&lt;/code&gt; block WordPress writes out by default is built from four elements working together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# BEGIN WordPress&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="sr"&gt; mod_rewrite.c&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="nc"&gt;RewriteEngine&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;
&lt;span class="nc"&gt;RewriteBase&lt;/span&gt; /blog/
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^index\.php$ - [L]
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{REQUEST_FILENAME} !-f
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{REQUEST_FILENAME} !-d
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; . /blog/index.php [L]
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="c"&gt;# END WordPress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RewriteEngine On&lt;/code&gt;&lt;/strong&gt; — turns on rewriting for this directory. Without it, every line below is ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RewriteCond&lt;/code&gt;&lt;/strong&gt; (condition) — sets a precondition for the &lt;code&gt;RewriteRule&lt;/code&gt; immediately following it. &lt;code&gt;%{REQUEST_FILENAME} !-f&lt;/code&gt; means "the requested path is &lt;strong&gt;not&lt;/strong&gt; an existing file," and &lt;code&gt;!-d&lt;/code&gt; means "not an existing directory." The rule below only fires when both conditions hold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RewriteRule&lt;/code&gt;&lt;/strong&gt; (the rewrite itself) — takes the form &lt;code&gt;pattern target [flags]&lt;/code&gt;. &lt;code&gt;.&lt;/code&gt; matches "one or more of any character" — essentially any path — and forwards it internally to &lt;code&gt;/blog/index.php&lt;/code&gt;. That single line is the actual mechanism behind WordPress's pretty permalinks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;[L]&lt;/code&gt; flag&lt;/strong&gt; — short for "Last." Once this rule applies, Apache stops evaluating further rules, which prevents a chain of rules from double-rewriting the same request in unintended ways.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other flags worth knowing: &lt;code&gt;[R=301]&lt;/code&gt; (returns a genuine permanent redirect the browser follows, changing the visible URL, as opposed to a silent internal forward), &lt;code&gt;[NC]&lt;/code&gt; (case-insensitive matching), and &lt;code&gt;[QSA]&lt;/code&gt; (appends the original query string onto the rewritten target instead of dropping it). A plain internal rewrite and an &lt;code&gt;[R=301]&lt;/code&gt;-flagged redirect can look superficially similar in a rule file but behave completely differently — that distinction is worth keeping straight.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;RewriteBase&lt;/code&gt; — the Anchor Point for Relative Paths
&lt;/h2&gt;

&lt;p&gt;The line most often overlooked is &lt;code&gt;RewriteBase /blog/&lt;/code&gt;. When a &lt;code&gt;RewriteRule&lt;/code&gt;'s target is a &lt;strong&gt;relative&lt;/strong&gt; path (one that doesn't start with &lt;code&gt;/&lt;/code&gt;), Apache resolves it against this &lt;code&gt;RewriteBase&lt;/code&gt; value. The WordPress installer generates this value automatically, based on whatever URL path the install is actually running at (its &lt;code&gt;home_url&lt;/code&gt;). That works fine under normal conditions — but if the &lt;strong&gt;server structure changes after installation&lt;/strong&gt;, this one value can be left pointing at a path that no longer exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Example — The Trap We Hit During a Subdomain Migration
&lt;/h2&gt;

&lt;p&gt;We hit exactly this trap while setting up the English version of this blog (&lt;code&gt;en.wpmm.jp/blog/&lt;/code&gt;). Here's how it played out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We first installed WordPress at &lt;code&gt;wpmm.jp/public_html/en/blog/&lt;/code&gt; through the hosting provider's one-click installer. At that point the site's URL structure was &lt;code&gt;wpmm.jp/en/blog/&lt;/code&gt; — a subdirectory under the main domain.&lt;/li&gt;
&lt;li&gt;We then created a &lt;strong&gt;subdomain&lt;/strong&gt;, &lt;code&gt;en.wpmm.jp&lt;/code&gt;, and pointed its docroot at that same &lt;code&gt;wpmm.jp/public_html/en/&lt;/code&gt; directory. The visible URL structure now became &lt;code&gt;en.wpmm.jp/blog/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We updated WordPress's internal URL settings to match the new subdomain via &lt;code&gt;wp option update home&lt;/code&gt; and &lt;code&gt;wp option update siteurl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;But &lt;code&gt;.htaccess&lt;/code&gt;'s &lt;code&gt;RewriteBase&lt;/code&gt; was still holding the value from step 1: &lt;code&gt;/en/blog/&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What happened next was interesting. The homepage (a near-direct hit on &lt;code&gt;index.php&lt;/code&gt;) loaded fine, but every individual post, category archive, search result, and 404 page came back as a &lt;strong&gt;500 error&lt;/strong&gt;. The homepage largely bypasses &lt;code&gt;mod_rewrite&lt;/code&gt; and resolves close to a direct file hit, while any permalinked URL has to pass through the &lt;code&gt;RewriteRule&lt;/code&gt; internal forward. When that forward resolved paths against the stale &lt;code&gt;RewriteBase /en/blog/&lt;/code&gt;, the result no longer matched the actual document root structure, and WordPress couldn't recognize the incoming request as a valid URL.&lt;/p&gt;

&lt;p&gt;One more wrinkle: running &lt;code&gt;wp rewrite flush --hard&lt;/code&gt; — the WP-CLI command meant to regenerate WordPress's internal routing rules and write them back to &lt;code&gt;.htaccess&lt;/code&gt; — &lt;strong&gt;didn't fix the file&lt;/strong&gt;. The shared hosting environment's permission constraints returned a warning ("Regenerating a .htaccess file requires special configuration") without actually modifying anything. The fix ended up being simple but manual: back up &lt;code&gt;.htaccess&lt;/code&gt;, then hand-edit &lt;code&gt;RewriteBase&lt;/code&gt; to the correct path (&lt;code&gt;/blog/&lt;/code&gt;).&lt;/p&gt;

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

&lt;p&gt;The takeaway generalizes beyond mod_rewrite specifically, to auto-generated configuration files as a category: &lt;strong&gt;a config file generated at install time captures a snapshot of the conditions that existed at that moment — it doesn't automatically track the infrastructure changing underneath it later.&lt;/strong&gt; Even a "regenerate" command like &lt;code&gt;wp rewrite flush&lt;/code&gt; can't be trusted blindly, since the permissions of the environment it runs in may quietly prevent it from touching the actual file. After any infrastructure change — subdomain migration, directory move, domain switch — opening &lt;code&gt;.htaccess&lt;/code&gt; directly and visually confirming that &lt;code&gt;RewriteBase&lt;/code&gt; still matches the real path structure is a small extra step that turns out to be the most reliable way to catch this class of bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RewriteEngine On&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turns on rewriting for this directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RewriteCond&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets a precondition for the &lt;code&gt;RewriteRule&lt;/code&gt; below it (e.g., only if not an existing file/directory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RewriteRule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forwards or redirects matching requests to another path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RewriteBase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The anchor directory relative paths resolve against — a value frozen at install time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[L]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops evaluating further rules once this one matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[R=301]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns a real permanent redirect (visible URL change), not a silent internal forward&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;.htaccess&lt;/code&gt; reads like an incantation at first glance, but it's decodable once you know what these four elements are each responsible for. &lt;code&gt;RewriteBase&lt;/code&gt; in particular is worth checking deliberately any time the underlying infrastructure changes — it's exactly the kind of value that gets left behind.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
