<?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>The Basics of `$wpdb-&gt;prepare()` — Why You Should Never Build SQL Queries With String Concatenation</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Mon, 19 Oct 2026 23:44:15 +0000</pubDate>
      <link>https://dev.to/susumun/the-basics-of-wpdb-prepare-why-you-should-never-build-sql-queries-with-string-concatenation-4l18</link>
      <guid>https://dev.to/susumun/the-basics-of-wpdb-prepare-why-you-should-never-build-sql-queries-with-string-concatenation-4l18</guid>
      <description>&lt;p&gt;When a WordPress plugin or theme needs to talk to the database directly, it uses the core-provided global object &lt;code&gt;$wpdb&lt;/code&gt;. The one thing to absolutely avoid when doing so is dropping user input straight into a SQL string through concatenation. The tool WordPress provides instead is &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;. This post looks at why that matters and what the function actually does under the hood.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: SQL injection is a broad term for attacks where unexpected input gets mixed into a SQL statement an application builds, rewriting what that statement actually tells the database to do. Any value coming from outside the application — form fields, URL parameters, and so on — is the starting point to be suspicious of.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why String Concatenation Is Dangerous
&lt;/h2&gt;

&lt;p&gt;Consider the following code (purely illustrative pseudo-code, not something to actually run on a live WordPress site):&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="c1"&gt;// Dangerous: user input is dropped straight into the string&lt;/span&gt;
&lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; WHERE post_title = '&lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine as long as &lt;code&gt;$title&lt;/code&gt; is an ordinary string. But if &lt;code&gt;$title&lt;/code&gt; is set to something like &lt;code&gt;' OR '1'='1&lt;/code&gt;, the resulting SQL statement becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;post_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'1'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;'1'='1'&lt;/code&gt; is always true, the &lt;code&gt;WHERE&lt;/code&gt; clause is effectively neutralized, and a query meant to return one specific post ends up returning every post instead. That's a simple example, but the same idea can be extended toward retrieving, modifying, or deleting data the application never intended to expose. &lt;strong&gt;The root cause is the act of building a SQL statement out of a value that came from outside the application.&lt;/strong&gt; No amount of careful checking of the value's contents removes the underlying risk as long as the statement itself is assembled through string concatenation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt; Actually Solves
&lt;/h2&gt;

&lt;p&gt;The solution WordPress core provides is &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;, used like this:&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="c1"&gt;// Safe: use a placeholder instead&lt;/span&gt;
&lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"SELECT * FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; WHERE post_title = %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;%s&lt;/code&gt; is a placeholder, and the value passed as the second argument gets inserted there. The key point is that &lt;code&gt;prepare()&lt;/code&gt; isn't doing simple string substitution — it &lt;strong&gt;escapes any special characters in the value first, so they can never be interpreted as part of the SQL statement's structure, before inserting it.&lt;/strong&gt; Pass the same &lt;code&gt;' OR '1'='1&lt;/code&gt; value through &lt;code&gt;prepare()&lt;/code&gt;, and it's treated purely as a literal string to search for; the structure of the SQL statement itself never changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Placeholder Type Matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt; supports a few main placeholder types.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Placeholder&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&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;Integer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Floating-point number&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The reason to use &lt;code&gt;%d&lt;/code&gt; or &lt;code&gt;%f&lt;/code&gt; isn't only escaping — it's that the value gets &lt;strong&gt;coerced into the expected type.&lt;/strong&gt; For a post ID field using &lt;code&gt;%d&lt;/code&gt;, a value like &lt;code&gt;123abc&lt;/code&gt; would simply become the integer &lt;code&gt;123&lt;/code&gt;. Using &lt;code&gt;%s&lt;/code&gt; where a number is expected, on the other hand, wraps the value in quotes as a string literal in the SQL statement, which can lead to unintended behavior or errors. Choosing the placeholder type that matches the meaning of the value being passed is just as important as using &lt;code&gt;prepare()&lt;/code&gt; in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Plain Placeholders Fall Short: &lt;code&gt;LIKE&lt;/code&gt; and &lt;code&gt;IN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LIKE&lt;/code&gt; clauses, commonly used for search features, are a well-known spot where the basic use of &lt;code&gt;prepare()&lt;/code&gt; isn't quite enough on its own. &lt;code&gt;LIKE&lt;/code&gt; interprets &lt;code&gt;%&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; as wildcards meaning "any string" and "any single character" respectively, so if a search keyword happens to contain those characters, the query can produce results the user never intended. WordPress core addresses this with a dedicated function, &lt;code&gt;$wpdb-&amp;gt;esc_like()&lt;/code&gt;, which escapes those wildcard characters before the value is handed to &lt;code&gt;prepare()&lt;/code&gt;.&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;$keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;esc_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'s'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"SELECT * FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; WHERE post_title LIKE %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$keyword&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, passing a list of values into an &lt;code&gt;IN (...)&lt;/code&gt; clause requires building out a placeholder for each value dynamically — it can't be handled with a single &lt;code&gt;%s&lt;/code&gt;. Being aware that a handful of patterns like these don't map cleanly onto the basic placeholder syntax is worth keeping in mind whenever &lt;code&gt;$wpdb&lt;/code&gt; is involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Theme Never Touches &lt;code&gt;$wpdb&lt;/code&gt; At All
&lt;/h2&gt;

&lt;p&gt;Looking through the implementation of this blog's theme, &lt;code&gt;wpmm-blog&lt;/code&gt;, &lt;code&gt;$wpdb&lt;/code&gt; is never referenced directly anywhere — not in &lt;code&gt;functions.php&lt;/code&gt;, not in any template file. Post listings, archives, and search results are all retrieved through &lt;code&gt;WP_Query&lt;/code&gt; and template tags (&lt;code&gt;have_posts()&lt;/code&gt;, &lt;code&gt;the_post()&lt;/code&gt;, and so on), so there's simply no point in the theme where a raw SQL statement needs to be assembled by hand.&lt;/p&gt;

&lt;p&gt;That's not an accident — it reflects a design principle recommended throughout WordPress theme and plugin development. High-level APIs like &lt;code&gt;WP_Query&lt;/code&gt; already perform the equivalent of &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt; internally, so the calling code never has to think about SQL at all. &lt;strong&gt;Reaching for the core-provided high-level API instead of writing raw SQL, wherever that's an option&lt;/strong&gt;, is just as practical a guideline for avoiding SQL injection as using &lt;code&gt;prepare()&lt;/code&gt; correctly when raw SQL genuinely is needed.&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;Situation&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inserting an ordinary value into a SQL statement&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt; with the right placeholder (&lt;code&gt;%s&lt;/code&gt; / &lt;code&gt;%d&lt;/code&gt; / &lt;code&gt;%f&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A value used inside a &lt;code&gt;LIKE&lt;/code&gt; clause&lt;/td&gt;
&lt;td&gt;Escape wildcards first with &lt;code&gt;$wpdb-&amp;gt;esc_like()&lt;/code&gt;, then pass the result to &lt;code&gt;prepare()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A situation where raw SQL can be avoided entirely&lt;/td&gt;
&lt;td&gt;Let a core API like &lt;code&gt;WP_Query&lt;/code&gt; handle it instead of assembling a query by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The core of SQL injection defense is turning the mindset of "never trust input" into a concrete habit: using &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;. It's an easy problem to miss if string concatenation has become second nature, so whenever you're reading or writing code that touches &lt;code&gt;$wpdb&lt;/code&gt;, it's worth making a habit of checking first whether placeholders are actually being used.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>WordPress Hooks Explained: How Plugins and Themes Extend Core Without Editing It</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Mon, 19 Oct 2026 00:18:56 +0000</pubDate>
      <link>https://dev.to/susumun/wordpress-hooks-explained-how-plugins-and-themes-extend-core-without-editing-it-3pef</link>
      <guid>https://dev.to/susumun/wordpress-hooks-explained-how-plugins-and-themes-extend-core-without-editing-it-3pef</guid>
      <description>&lt;p&gt;WordPress plugins and themes change what gets displayed and how data gets processed without ever touching WordPress core's own files (the code under &lt;code&gt;wp-includes&lt;/code&gt; / &lt;code&gt;wp-admin&lt;/code&gt;). What makes that possible is a mechanism called &lt;strong&gt;hooks&lt;/strong&gt;, which come in two flavors: &lt;strong&gt;actions&lt;/strong&gt; and &lt;strong&gt;filters&lt;/strong&gt;. This blog's own theme, &lt;code&gt;wpmm-blog&lt;/code&gt;, uses both extensively in its &lt;code&gt;functions.php&lt;/code&gt;, so this article walks through real code from it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: a hook is a point WordPress core deliberately builds in for outside code to plug into. At specific points in core's own source, calls like &lt;code&gt;do_action()&lt;/code&gt; and &lt;code&gt;apply_filters()&lt;/code&gt; are already embedded. Plugins and themes "hook" their own functions onto those points to intercept what core does there, without editing core itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What separates an action from a filter
&lt;/h2&gt;

&lt;p&gt;The two hook types split cleanly along one line: whether they return a value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: runs some code at a given moment. It doesn't need to return anything — it's for side effects (writing a log, saving data, making an outbound request)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt;: receives a value core already has, transforms it, and hands it back. It's for changing data right before core uses or displays it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: an action interrupts a moment to do something; a filter rewrites a value and returns it. Once you know which of the two your code needs to do, whether to register it with &lt;code&gt;add_action()&lt;/code&gt; or &lt;code&gt;add_filter()&lt;/code&gt; follows naturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  A filter example: excerpt length and the "read more" marker
&lt;/h2&gt;

&lt;p&gt;WordPress core truncates the excerpt shown on archive pages to 55 words by default, appending &lt;code&gt;[&amp;amp;hellip;]&lt;/code&gt; at the end. Changing that behavior doesn't require touching core's source — it just means registering functions on two filter hooks, &lt;code&gt;excerpt_length&lt;/code&gt; and &lt;code&gt;excerpt_more&lt;/code&gt;.&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wpmm_blog_excerpt_length'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpmm_blog_excerpt_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Japanese content is measured in "words" here too, but 60&lt;/span&gt;
        &lt;span class="c1"&gt;// reads as a reasonable length even with full-width characters&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'excerpt_length'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpmm_blog_excerpt_length'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wpmm_blog_excerpt_more'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpmm_blog_excerpt_more&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$more&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;' …'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'excerpt_more'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpmm_blog_excerpt_more'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail: each function &lt;strong&gt;receives core's default value as an argument and returns a replacement&lt;/strong&gt;. &lt;code&gt;wpmm_blog_excerpt_length($length)&lt;/code&gt; gets core's default (55) as &lt;code&gt;$length&lt;/code&gt;, but this implementation ignores it and always returns a fixed 60. Core's internal logic takes whatever the function returns and uses that as the excerpt length going forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  An action example: pre-generating an OGP image on save
&lt;/h2&gt;

&lt;p&gt;For an action example, this blog pre-generates and caches a post's OGP (Open Graph) image at the moment a post is saved or published, hooked onto the &lt;code&gt;save_post&lt;/code&gt; action.&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="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'save_post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_wpmm_blog_pregenerate_ogp_on_publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&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;add_action()&lt;/code&gt; takes, in order: the hook name, the callback function, a priority, and the number of arguments the callback expects. This call uses priority 10 (the default) and requests 3 arguments.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;save_post&lt;/code&gt; is capable of passing three arguments to its callbacks — &lt;code&gt;$post_ID&lt;/code&gt;, &lt;code&gt;$post&lt;/code&gt; (the post object), and &lt;code&gt;$update&lt;/code&gt; (a boolean for whether this was an update vs. a new post) — but that capability only applies if the callback explicitly asks for that many; by default only the first is passed. The &lt;code&gt;3&lt;/code&gt; at the end of this call exists specifically so the callback can use all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why priority exists
&lt;/h2&gt;

&lt;p&gt;A widely-used hook like &lt;code&gt;save_post&lt;/code&gt; often has more than one plugin or theme registering a callback on it simultaneously, alongside core's own internal use of the same hook. When every callback shares the same default priority (10), they run in registration order — but real requirements sometimes call for "run after this other callback" or "run before everything else," and priority is the explicit numeric knob for that. Lower numbers run earlier; higher numbers run later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this means core never has to be edited directly
&lt;/h2&gt;

&lt;p&gt;WordPress core's own source has &lt;code&gt;do_action('hook-name', ...)&lt;/code&gt; (firing an action) and &lt;code&gt;apply_filters('hook-name', $value, ...)&lt;/code&gt; (applying a filter) calls built into it at deliberate points throughout. Plugins and themes register callbacks against those pre-built entry points with &lt;code&gt;add_action()&lt;/code&gt; / &lt;code&gt;add_filter()&lt;/code&gt; — they never modify core's files at all.&lt;/p&gt;

&lt;p&gt;That has a very concrete practical payoff. When WordPress core is updated, automatically or manually, its files get replaced wholesale with the new version's. Any customization made by editing core files directly would be silently wiped out on the next update. Customization made through hooks lives in its own file, under &lt;code&gt;wp-content/themes/&lt;/code&gt; or &lt;code&gt;wp-content/plugins/&lt;/code&gt;, entirely separate from core — so it survives core updates untouched.&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;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Return value&lt;/th&gt;
&lt;th&gt;Example from this blog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;Run code at a given moment&lt;/td&gt;
&lt;td&gt;None (side effects only)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;save_post&lt;/code&gt; → pre-generate the OGP image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter&lt;/td&gt;
&lt;td&gt;Transform a value core hands it&lt;/td&gt;
&lt;td&gt;The transformed value&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;excerpt_length&lt;/code&gt; → change the excerpt word count to 60&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Actions and filters, together, are the entire mechanism that lets WordPress core, plugins, and themes cooperate without any of the three ever editing another's source. When reading unfamiliar WordPress code, checking just two things — is this &lt;code&gt;add_action&lt;/code&gt; or &lt;code&gt;add_filter&lt;/code&gt;, and which hook name is it registered on — is usually enough to figure out exactly where in core's flow it's intercepting, and how.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Timezone Conversion Pitfalls: Where UTC/JST Bugs Actually Come From</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 01:24:52 +0000</pubDate>
      <link>https://dev.to/susumun/timezone-conversion-pitfalls-where-utcjst-bugs-actually-come-from-15ji</link>
      <guid>https://dev.to/susumun/timezone-conversion-pitfalls-where-utcjst-bugs-actually-come-from-15ji</guid>
      <description>&lt;p&gt;Japan Standard Time (JST) is a fixed UTC+9 offset with no daylight saving time to complicate it. That simplicity makes it tempting to assume timezone handling is "just add or subtract nine hours." In practice, most timezone bugs don't come from getting that arithmetic wrong — they come from losing track of which basis a given timestamp is even using in the first place. This article walks through three real examples, drawn from actual code, logging, and this very blog's own publishing workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 1: values that don't carry their own timezone
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: a "naive" datetime is a date/time value with no timezone information attached at all. An "aware" datetime is one that carries that information.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Python's &lt;code&gt;datetime.now()&lt;/code&gt; returns the current time in the server's local timezone, but the returned &lt;code&gt;datetime&lt;/code&gt; object itself carries no tag saying "this is JST." &lt;code&gt;datetime.utcnow()&lt;/code&gt; has the same problem in reverse — it returns UTC, but nothing about the value says so.&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;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;local_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# e.g. 2026-09-15 14:30:00, but nothing says "JST"
&lt;/span&gt;&lt;span class="n"&gt;utc_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;# e.g. 2026-09-15 05:30:00, but nothing says "UTC"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put these two side by side and there's no way to tell which is which without knowing how the code that produced them was written. Log one and persist the other to a database, and whoever reads it later — including you, months from now — has no way to know which basis it's in.&lt;/p&gt;

&lt;p&gt;That's exactly the shape of bug that slips past review. A value that's off by nine hours still looks like a perfectly plausible timestamp on its own; the mistake only becomes visible once you compare it against something known to be correct.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;datetime.utcnow()&lt;/code&gt; was deprecated in Python 3.12, for precisely this reason — a naive value's timezone isn't self-evident. The replacement, &lt;code&gt;datetime.now(timezone.utc)&lt;/code&gt;, returns a value that carries "this is UTC" as part of itself (an aware datetime).&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;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;utc_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# tzinfo=UTC travels with the value
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pitfall 2: log timestamps default to local time
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/log-level-design-and-rotation/" rel="noopener noreferrer"&gt;An earlier article on log-level design&lt;/a&gt; used &lt;code&gt;maintenance_agent.py&lt;/code&gt;'s &lt;code&gt;logging.Formatter&lt;/code&gt; as an example. The &lt;code&gt;%(asctime)s&lt;/code&gt; that formatter produces is built, by default, using &lt;code&gt;time.localtime&lt;/code&gt; — the server's own local timezone. That's Python's &lt;code&gt;logging&lt;/code&gt; module default, not something the code opts into.&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;_log_fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s - [%(levelname)s] - %(message)s&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;Run a single server, always on JST, and this never causes a problem — the log timestamps just read as JST. But once logs from multiple servers (which may not share a timezone) get aggregated for a single timeline, or a JST server's logs get compared against a UTC-based cloud environment's logs during an incident, plain timestamp strings with no explicit timezone can't be reliably lined up against each other.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;logging.Formatter&lt;/code&gt; exposes a &lt;code&gt;converter&lt;/code&gt; attribute that can be swapped to &lt;code&gt;time.gmtime&lt;/code&gt; to make &lt;code&gt;asctime&lt;/code&gt; UTC-based instead.&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;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="n"&gt;_log_fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s - [%(levelname)s] - %(message)s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_log_fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;converter&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="n"&gt;gmtime&lt;/span&gt;  &lt;span class="c1"&gt;# asctime is now UTC-based
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither choice is inherently correct — what matters is that the basis a given log's timestamps use is stated somewhere, in the code or the runbook, rather than left implicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 3: WordPress's &lt;code&gt;post_date&lt;/code&gt; and &lt;code&gt;post_date_gmt&lt;/code&gt; split
&lt;/h2&gt;

&lt;p&gt;This blog's own publishing workflow has run into exactly this category of bug. WordPress stores a post's timestamp in &lt;strong&gt;two separate columns&lt;/strong&gt;: &lt;code&gt;post_date&lt;/code&gt; (local time, in whatever timezone the site is configured for — JST, for this blog) and &lt;code&gt;post_date_gmt&lt;/code&gt; (the same instant, expressed in UTC).&lt;/p&gt;

&lt;p&gt;What happens if a &lt;code&gt;wp post create&lt;/code&gt; call only sets one of the two correctly? This blog hit that in practice: a post's &lt;code&gt;--post_date&lt;/code&gt; was correctly set to a past JST time, but &lt;code&gt;--post_date_gmt&lt;/code&gt; was left unset (or ended up holding the same value as the JST one). WordPress interpreted that as "this post is scheduled for a time still in the future," silently flipped its status to &lt;code&gt;future&lt;/code&gt;, and the page started returning a 404 — even though the intent was to publish immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# safe publish command: set both post_date (JST) and post_date_gmt (UTC) explicitly&lt;/span&gt;
wp post create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--post_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;publish &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--post_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-09-15 14:00:00'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--post_date_gmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-09-15 05:00:00'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root cause is simple: &lt;code&gt;post_date_gmt&lt;/code&gt; is supposed to hold &lt;code&gt;post_date&lt;/code&gt; minus nine hours, but if WordPress's default logic or a missing CLI flag lets the JST value get stored there as-is, it ends up nine hours ahead of the real UTC time — which reads as "still in the future." Nine hours happens to be exactly the JST/UTC offset, so whenever a post mysteriously gets treated as nine hours ahead of schedule, this &lt;code&gt;post_date&lt;/code&gt; / &lt;code&gt;post_date_gmt&lt;/code&gt; mismatch is the first thing worth checking. The fix is just as simple: always set both fields explicitly, nine hours apart, on every create or update call.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: WordPress keeps these as two columns because they serve different purposes — &lt;code&gt;post_date&lt;/code&gt; is the human-facing time an admin sees in the dashboard, and &lt;code&gt;post_date_gmt&lt;/code&gt; is the timezone-independent value plugins and API integrations rely on. The REST API reflects the same split, returning both &lt;code&gt;date&lt;/code&gt; (site timezone) and &lt;code&gt;date_gmt&lt;/code&gt; (UTC) for every post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What these three have in common
&lt;/h2&gt;

&lt;p&gt;These three pitfalls look like unrelated technologies on the surface — plain Python datetimes, a logging formatter, a pair of WordPress database columns — but they share the same root cause. &lt;strong&gt;Some value's timezone basis simply isn't recoverable from the value itself, and something downstream assumes it is.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pitfall&lt;/th&gt;
&lt;th&gt;Where it shows up&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Naive datetimes&lt;/td&gt;
&lt;td&gt;Python code&lt;/td&gt;
&lt;td&gt;Comparing or storing values without knowing which basis each one is in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local-time log timestamps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;logging.Formatter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Logs from different servers/environments can't be lined up on a shared timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;post_date&lt;/code&gt;/&lt;code&gt;post_date_gmt&lt;/code&gt; mismatch&lt;/td&gt;
&lt;td&gt;WordPress publishing&lt;/td&gt;
&lt;td&gt;Post misread as nine hours in the future; status flips to &lt;code&gt;future&lt;/code&gt;, page 404s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to avoid it
&lt;/h2&gt;

&lt;p&gt;The fixes all point the same direction — not a clever trick, so much as consistently picking one basis and making sure every value carries it explicitly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep everything internal in UTC, and convert to local time only at the point where a human reads it&lt;/strong&gt; — sometimes phrased as "UTC in the core, local at the edge." Converting too early, or in more than one place, is exactly how double-conversion and missed-conversion bugs creep in&lt;/li&gt;
&lt;li&gt;In Python, avoid producing naive &lt;code&gt;datetime&lt;/code&gt; values in the first place; prefer &lt;code&gt;datetime.now(timezone.utc)&lt;/code&gt; so the timezone travels with the value instead of living only in the programmer's memory&lt;/li&gt;
&lt;li&gt;Where a value's basis genuinely isn't self-evident — logs, external integrations — name it explicitly (a &lt;code&gt;_utc&lt;/code&gt; or &lt;code&gt;_jst&lt;/code&gt; suffix on a variable, a line in the docs) so the basis is visible without having to trace back through the code&lt;/li&gt;
&lt;li&gt;For systems like WordPress that keep the same instant in two separate columns, never write a command that updates just one of them. Every create or update call should set both, together, every time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JST's fixed UTC+9 offset with no DST makes the arithmetic trivial, but that same simplicity breeds a kind of complacency — "it's just nine hours either way." In practice, the failures don't come from getting the arithmetic wrong. They come from losing track of which basis a given value was in to begin with, and that's the thing worth watching for.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Optimistic vs. Pessimistic Locking: Two Ways to Stop Database Updates From Colliding</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Fri, 16 Oct 2026 00:23:36 +0000</pubDate>
      <link>https://dev.to/susumun/optimistic-vs-pessimistic-locking-two-ways-to-stop-database-updates-from-colliding-3flk</link>
      <guid>https://dev.to/susumun/optimistic-vs-pessimistic-locking-two-ways-to-stop-database-updates-from-colliding-3flk</guid>
      <description>&lt;p&gt;When two people or processes update the same piece of data at nearly the same time, one can end up overwriting the other's change without ever knowing it happened. This is the classic "lost update" problem, and it shows up anywhere a database, or any multi-process application, allows concurrent writes. There are two broad strategies for handling it: pessimistic locking and optimistic locking. The names are opposites, and so is the approach each one takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "silent overwrite" problem
&lt;/h2&gt;

&lt;p&gt;Say a table tracks inventory, and two processes, A and B, both read the same row at nearly the same moment. Both see "10 units in stock" and both compute "subtract one, so it should become 9." With no safeguard, whichever update runs last simply writes 9 — even though two separate decrements should have produced 8. One update vanishes without a trace.&lt;/p&gt;

&lt;p&gt;There are two ways to prevent that: don't let anyone else touch the data while you're working on it (pessimistic locking), or let everyone touch it, but check for contradictions before committing (optimistic locking).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pessimistic locking: lock first, then touch the data
&lt;/h2&gt;

&lt;p&gt;Pessimistic locking grabs an exclusive lock the moment it reads a row, and makes every other process wait until it's done. The assumption baked into the name — "someone else is probably going to try to touch this too" — is why you lock it down before doing anything else.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;FileLock&lt;/code&gt; class in this project's own &lt;code&gt;core/file_lock.py&lt;/code&gt; is a real example. The GUI (&lt;code&gt;site_manager_web.py&lt;/code&gt;) and a background maintenance run (&lt;code&gt;maintenance_agent.py&lt;/code&gt;) can both try to write the same config file (&lt;code&gt;sites_*.json&lt;/code&gt; and similar) at once, so the code takes an exclusive lock before writing and makes the other process wait until it's released.&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;core.file_lock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileLock&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;FileLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sites_default.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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# anything else trying to touch this file waits here
&lt;/span&gt;    &lt;span class="nf"&gt;_atomic_write_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sites_default.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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/python-file-locking-fcntl-vs-msvcrt/" rel="noopener noreferrer"&gt;An earlier article&lt;/a&gt; covered how that lock is implemented at the OS level — &lt;code&gt;fcntl&lt;/code&gt; on Unix versus &lt;code&gt;msvcrt&lt;/code&gt; on Windows. This one is a level up: the design decision to take a lock before writing at all, not the mechanics of how the lock itself works.&lt;/p&gt;

&lt;p&gt;On the database side, most RDBMSes offer pessimistic locking through a &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; clause.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- any other transaction trying to update this row now blocks here&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: a deadlock happens when two transactions each hold a lock the other is waiting for, so neither can proceed. Pessimistic locking reliably prevents lost updates, but getting the lock order wrong can produce exactly this situation. Most RDBMSes detect deadlocks and force one transaction to fail, leaving retry logic to the application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The downside is that everyone else waits while a lock is held. With long-running operations or heavy concurrent access, that waiting adds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimistic locking: assume conflicts are rare, check afterward
&lt;/h2&gt;

&lt;p&gt;Optimistic locking takes the opposite view: "two processes updating the same row at once probably won't happen often," so it never locks on read. Instead, it checks at write time whether the row has changed since it was read.&lt;/p&gt;

&lt;p&gt;The usual implementation adds a &lt;code&gt;version&lt;/code&gt; or &lt;code&gt;updated_at&lt;/code&gt; column and includes that value in the update's &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this affects exactly one row, nobody touched it since it was read, and the update succeeds. If it affects zero rows, the &lt;code&gt;version&lt;/code&gt; no longer matches — someone else got there first. The application treats that as a detected conflict: re-read the current value and retry, or tell the user their change collided with someone else's.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: "conflict" here means two attempted changes to the same data that can't both be applied. Optimistic locking doesn't prevent conflicts — it detects, at write time, whether one actually occurred.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same idea shows up on the web. &lt;a href="https://en.wpmm.jp/blog/cache-control-vs-etag-what-they-control/" rel="noopener noreferrer"&gt;An earlier article on HTTP cache headers&lt;/a&gt; covered &lt;code&gt;ETag&lt;/code&gt;, which normally validates a cache. Paired with an &lt;code&gt;If-Match&lt;/code&gt; header, though, it becomes a form of optimistic locking: "only accept this update if the version I last fetched still matches what's on the server now." A client sends &lt;code&gt;If-Match: "&amp;lt;its last-fetched ETag&amp;gt;"&lt;/code&gt; on an update request, and the server rejects the write if the current ETag doesn't match.&lt;/p&gt;

&lt;h2&gt;
  
  
  WordPress's "post lock" isn't strictly pessimistic locking
&lt;/h2&gt;

&lt;p&gt;WordPress's admin screen has a familiar warning: open a post someone else is already editing, and you'll see "So-and-so is currently editing this post." It's built on the Heartbeat API, which periodically writes and refreshes a lock transient marking who's editing.&lt;/p&gt;

&lt;p&gt;But this isn't a hard, database-enforced lock. It's advisory — a notice, not a barrier. If both users ignore it and hit save, &lt;code&gt;wp_update_post()&lt;/code&gt; has no built-in conflict detection, so whichever save happens last simply wins. The UI flags that someone else is in there, but nothing at the database level actually prevents a lost update — so saving anyway, warning ignored, is expected behavior rather than a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the two
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Pessimistic locking&lt;/th&gt;
&lt;th&gt;Optimistic locking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Assumption&lt;/td&gt;
&lt;td&gt;Conflicts are common&lt;/td&gt;
&lt;td&gt;Conflicts are rare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When it acts&lt;/td&gt;
&lt;td&gt;Locks on read&lt;/td&gt;
&lt;td&gt;Detects conflicts on write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect on others&lt;/td&gt;
&lt;td&gt;Others wait while the lock is held&lt;/td&gt;
&lt;td&gt;No waiting under normal conditions; retries only on conflict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fits best&lt;/td&gt;
&lt;td&gt;High update frequency, expensive-to-redo failures&lt;/td&gt;
&lt;td&gt;Read-heavy workloads with few write collisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main risk&lt;/td&gt;
&lt;td&gt;Deadlock&lt;/td&gt;
&lt;td&gt;Missing retry logic after a detected conflict&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which one to use
&lt;/h2&gt;

&lt;p&gt;When conflicts happen often, or redoing a failed update is expensive (inventory allocation, say, where retrying gets complicated), locking up front is usually easier to reason about. When reads dominate and write collisions are rare, pessimistic locking's constant waiting becomes a drag, and optimistic locking fits better.&lt;/p&gt;

&lt;p&gt;This project uses &lt;code&gt;FileLock&lt;/code&gt; — pessimistic — for config writes because the GUI and background process rarely collide on the same file, so the occasional wait doesn't matter, while a corrupted partial write (an incomplete JSON file) is worth avoiding outright. The lock is also held only briefly, just long enough to finish one file. Pessimistic locking isn't the right answer everywhere; it comes down to how often conflicts happen, how costly a failed retry is, and whether waiting is acceptable.&lt;/p&gt;

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

&lt;p&gt;Pessimistic locking locks on read and makes everyone else wait, trading that wait — and some deadlock risk — for guaranteed conflict prevention. Optimistic locking skips the lock and checks at write time whether the version changed, avoiding waiting under normal conditions but requiring retry handling once a conflict is detected. This project's &lt;code&gt;FileLock&lt;/code&gt; is a real instance of the former; HTTP's &lt;code&gt;ETag&lt;/code&gt; paired with &lt;code&gt;If-Match&lt;/code&gt; is a web-native version of the latter. Both answer the same question: what happens when more than one process reaches for the same data at once.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Git Bisect and Blame: Binary-Searching Your Way to "When Did This Break?"</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 15 Oct 2026 00:24:57 +0000</pubDate>
      <link>https://dev.to/susumun/git-bisect-and-blame-binary-searching-your-way-to-when-did-this-break-18g6</link>
      <guid>https://dev.to/susumun/git-bisect-and-blame-binary-searching-your-way-to-when-did-this-break-18g6</guid>
      <description>&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/git-staging-area-add-commit-explained/" rel="noopener noreferrer"&gt;Last time&lt;/a&gt; we looked at what actually happens between &lt;code&gt;git add&lt;/code&gt; and &lt;code&gt;git commit&lt;/code&gt; — the mechanics of the moment a commit gets created. This time we shift focus to a different kind of question: given a long history of past commits, how do you track down the one that introduced a specific bug? &lt;code&gt;git blame&lt;/code&gt; and &lt;code&gt;git bisect&lt;/code&gt; both dig into that history, but they're suited to very different situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;git blame&lt;/code&gt;: following one line's history directly
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;git blame &amp;lt;file&amp;gt;&lt;/code&gt; lists, for every line in that file, the most recent commit that touched it — hash, author, and date included. If you already suspect a particular line is causing a problem, &lt;code&gt;blame&lt;/code&gt; tells you exactly when it was introduced and by which commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git blame &lt;span class="nt"&gt;-L&lt;/span&gt; 40,60 core/db_backup_diagnostics.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Narrowing the range with &lt;code&gt;-L&lt;/code&gt; keeps the search fast since you don't have to scan the whole file. From there, feeding the commit hash into &lt;code&gt;git show &amp;lt;hash&amp;gt;&lt;/code&gt; reveals the full commit message and diff that line was part of. The limitation of &lt;code&gt;blame&lt;/code&gt; is that it tells you when a line was written, not whether that line is actually responsible for the bug you're chasing. If you don't yet know which line is at fault, &lt;code&gt;blame&lt;/code&gt; isn't the right starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;git bisect&lt;/code&gt;: binary-searching between a known-good point and a known-bad one
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git bisect&lt;/code&gt; is the tool for when you don't know which commit — or even which file — caused the regression. The idea is simple: mark one commit you know was working ("good") and one you know is currently broken ("bad"). Git then checks out the commit sitting roughly halfway between them. You test that commit and tell Git whether it's still broken or not, and the remaining range gets cut in half again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: binary search means repeatedly checking the midpoint of a range and discarding the half that can't contain the answer. Even with 100 candidates, this narrows things down to one in roughly seven checks (2 to the 7th power is 128).&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect start
git bisect bad                      &lt;span class="c"&gt;# the current commit (HEAD) is broken&lt;/span&gt;
git bisect good v1.6.8               &lt;span class="c"&gt;# this tag was known to work&lt;/span&gt;
&lt;span class="c"&gt;# → Git checks out a commit roughly halfway between the two&lt;/span&gt;
&lt;span class="c"&gt;# test it, then report the result:&lt;/span&gt;
git bisect good   &lt;span class="c"&gt;# if it still works&lt;/span&gt;
git bisect bad    &lt;span class="c"&gt;# if it's broken&lt;/span&gt;
&lt;span class="c"&gt;# this repeats automatically until one commit remains&lt;/span&gt;
git bisect reset  &lt;span class="c"&gt;# return to your original branch when done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even across hundreds of candidate commits, this cuts the number of commits you actually need to check down sharply compared with testing them one by one in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating it: &lt;code&gt;git bisect run&lt;/code&gt; hands the verdict to a script
&lt;/h2&gt;

&lt;p&gt;You don't have to eyeball good/bad by hand every time. If you have a script or test that reliably reproduces the bug, pass it to &lt;code&gt;git bisect run &amp;lt;command&amp;gt;&lt;/code&gt;, and Git will execute that command at each candidate commit automatically — treating exit code 0 as good and anything else as bad.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect start
git bisect bad HEAD
git bisect good v1.6.8
git bisect run python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest tests.test_db_backup_diagnostics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it a valid starting good/bad pair, and Git drives the whole search to the culprit commit on its own. The catch is that your test needs to reliably reproduce the exact failure you're chasing. A flaky check (one that sometimes fails for unrelated reasons) breaks the core assumption of binary search — that one side of the range is definitively good and the other definitively bad — and can lead &lt;code&gt;bisect&lt;/code&gt; to converge on the wrong commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: tracking down a shell-dependent parsing bug
&lt;/h2&gt;

&lt;p&gt;There was a past issue where DB backups reliably failed on hosts using csh-family login shells, such as Sakura Internet's. The cause turned out to be a progress-log output routine added at some point that implicitly assumed a bash-style environment; under csh, the log format came out differently and parsing broke. This is a textbook case for &lt;code&gt;bisect&lt;/code&gt;: you know exactly when the symptom appeared (once a csh-environment report came in), but the actual commit responsible could be anywhere across dozens or hundreds of commits further back. With a test that reproduces the csh environment, &lt;code&gt;git bisect run&lt;/code&gt; can chase down the offending commit without manual intervention at every step; without one, you'd answer good/bad by hand after testing on that environment each time. Once &lt;code&gt;bisect&lt;/code&gt; narrows things to a single commit, &lt;code&gt;git blame&lt;/code&gt; (or &lt;code&gt;git show&lt;/code&gt; on that commit) is the natural next step to understand what the change was trying to do and how to fix it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for which
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You already suspect a specific line and want to know when it was written&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git blame&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Something is broken, but you don't know which line or commit caused it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git bisect&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bisect&lt;/code&gt; narrowed it to one commit and you want to understand the intent behind it&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git blame&lt;/code&gt; / &lt;code&gt;git show &amp;lt;commit&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once &lt;code&gt;bisect&lt;/code&gt; narrows a regression down to a single commit, reading that commit's diff with &lt;code&gt;git show&lt;/code&gt; — and tracing the same lines further back with &lt;code&gt;blame&lt;/code&gt; if needed — is the natural next step. It helps to think of &lt;code&gt;bisect&lt;/code&gt; as narrowing the range and &lt;code&gt;blame&lt;/code&gt; as digging into one specific point; they cover different stages of the same investigation.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;git blame&lt;/code&gt; tells you exactly when a specific line last changed, but it's no help if you don't yet know which line is at fault. &lt;code&gt;git bisect&lt;/code&gt; only needs a known-good and a known-bad commit to binary-search its way to the culprit, and pairing it with a reliable test via &lt;code&gt;git bisect run&lt;/code&gt; lets the whole search run unattended. This connects back to &lt;a href="https://en.wpmm.jp/blog/git-staging-area-add-commit-explained/" rel="noopener noreferrer"&gt;last time's point about staging&lt;/a&gt; — commits are fixed-in-time snapshots, and it's precisely because of that property that you can binary-search backward through them to find which snapshot introduced the problem.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>JWT and Session Tokens Explained: Stateless vs. Stateful Authentication</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Wed, 14 Oct 2026 01:26:26 +0000</pubDate>
      <link>https://dev.to/susumun/jwt-and-session-tokens-explained-stateless-vs-stateful-authentication-1d0p</link>
      <guid>https://dev.to/susumun/jwt-and-session-tokens-explained-stateless-vs-stateful-authentication-1d0p</guid>
      <description>&lt;p&gt;Two articles back, we covered &lt;a href="https://en.wpmm.jp/blog/wordpress-rest-api-cookie-nonce-authentication/" rel="noopener noreferrer"&gt;cookie+nonce authentication&lt;/a&gt;, and last time we looked at how &lt;a href="https://en.wpmm.jp/blog/wordpress-cookie-session-authentication-basics/" rel="noopener noreferrer"&gt;WordPress's login cookie&lt;/a&gt; is actually a hybrid: signature verification is fully self-contained, but revocation depends on a server-side list of valid tokens. This time we look at the opposite end of the design spectrum: JWT (JSON Web Token), which aims for authentication that is fully stateless — no server-side token list at all. Comparing it against WordPress's hybrid model makes the trade-off between stateful and stateless authentication much easier to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a JWT actually is: three dot-separated parts
&lt;/h2&gt;

&lt;p&gt;A JWT looks like a single string split into three parts by dots — &lt;code&gt;xxxxx.yyyyy.zzzzz&lt;/code&gt;. These correspond to a header, a payload, and a signature. The header names the signing algorithm (such as &lt;code&gt;HS256&lt;/code&gt;) and token type; the payload holds "claims" — pieces of data like a user ID, an expiration time, or permissions. The header and payload are each Base64url-encoded and joined with a dot, and the signature is computed over that combined string using the algorithm named in the header.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Base64url is a variant of ordinary Base64 that swaps out characters like &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, and &lt;code&gt;=&lt;/code&gt; for &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; so the result is safe to embed in URLs and cookies. It's just a character encoding, not encryption — anyone can decode it back to the original string instantly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A common misconception: "signed" is not "encrypted"
&lt;/h2&gt;

&lt;p&gt;A frequent misunderstanding is assuming the JWT payload is hidden because it's "encrypted." In reality it's only Base64url-encoded — there's no encryption involved at all. Anyone can decode it in a second using browser dev tools or a command line, and read the user ID, permissions, or any other claim inside directly. What a JWT actually guarantees is not confidentiality but integrity: that the content hasn't been tampered with since it was issued. This is the exact same property WordPress's authentication cookie relies on via its HMAC signature, as covered last time. The practical takeaway is the same in both cases: never put a password, API key, or other secret inside a JWT payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  No database lookup required — genuine statelessness
&lt;/h2&gt;

&lt;p&gt;WordPress's authentication cookie, as we saw last time, checks the HMAC signature and then also has to look up whether the token is still on the list of currently valid tokens stored as user metadata in the database. That lookup is exactly what makes "log out everywhere" possible — but it also means a database query happens on every single request.&lt;/p&gt;

&lt;p&gt;JWT is designed to skip that lookup entirely. Whoever needs to verify a token only needs the shared secret key (for HMAC-based signing) or the public key (for asymmetric algorithms like RSA or ECDSA) to recompute the signature and check it matches. No database query, and no round trip back to the issuing server, is required. This is exactly what "stateless" means here, and it's why JWT is popular in distributed systems where multiple microservices need to share the same authentication scheme — each service can verify a token entirely on its own, using only a key it already holds, without querying a central session store.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off: no way to instantly revoke a single token
&lt;/h2&gt;

&lt;p&gt;This statelessness comes at a clear cost. Once a JWT is issued, it will pass verification anywhere its signature checks out, right up until it expires — there's no equivalent of WordPress's "log out everywhere" for a plain JWT. The very design choice that eliminates the database lookup — not maintaining any server-side list of valid tokens — is also what removes the ability to forcibly invalidate one specific token before it expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world compromises: pure statelessness is hard to sustain
&lt;/h2&gt;

&lt;p&gt;Two common approaches address this weakness in practice. The first is to keep the JWT's own expiration very short — a few minutes to maybe an hour — and pair it with a separate, longer-lived "refresh token" that the server tracks and uses to reissue new JWTs periodically. This reintroduces a small amount of server-side state, in the form of managing the refresh token, but the short-lived JWT itself can still be verified statelessly. The second approach is to maintain a server-side blocklist of revoked token IDs (the &lt;code&gt;jti&lt;/code&gt; claim) and check every incoming token against it — which, in effect, brings back the same kind of token-list lookup WordPress relies on, giving up most of the statelessness advantage in the process. Between these two compromises, it becomes clear that being fully stateless and being able to revoke instantly are, in principle, difficult to have at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one fits — it depends on the system's shape
&lt;/h2&gt;

&lt;p&gt;In a system like WordPress, where a single central database already sits within reach of every request, the hybrid approach we covered last time makes good sense: the database round trip is already happening anyway, so adding one more lookup for the token list is a small cost for a genuinely useful feature — instant revocation.&lt;/p&gt;

&lt;p&gt;In a distributed system made of several independent services, or a mobile app talking to multiple separate backend APIs, having every request check in with one central session store can itself become a bottleneck or a network dependency. That's where JWT's "verify independently with just a key" property becomes the real advantage. Giving up instant per-token revocation in exchange for a short expiration plus a refresh token often makes the overall system easier to operate at that scale.&lt;/p&gt;

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

&lt;p&gt;A JWT is made of a header, a payload, and a signature; the payload is merely encoded, not encrypted, so it should never carry secrets. Its defining feature is genuine statelessness — verification relies purely on a signature check, with no server-side token list required — which is what makes it cheap to verify across distributed services. The cost is that no single token can be revoked instantly, which is why real deployments lean on short expirations paired with refresh tokens, or a blocklist that partially reintroduces state. Seen next to &lt;a href="https://en.wpmm.jp/blog/wordpress-cookie-session-authentication-basics/" rel="noopener noreferrer"&gt;WordPress's hybrid cookie model&lt;/a&gt; from last time, the two designs come into focus as different answers to the same underlying tension: fast, stateless verification versus the ability to revoke access on demand.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>CORS Explained: Why Browsers Block Cross-Origin API Requests</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 13 Oct 2026 01:25:22 +0000</pubDate>
      <link>https://dev.to/susumun/cors-explained-why-browsers-block-cross-origin-api-requests-3n1g</link>
      <guid>https://dev.to/susumun/cors-explained-why-browsers-block-cross-origin-api-requests-3n1g</guid>
      <description>&lt;p&gt;Anyone who has wired up a browser-side call to an API on a different domain has probably hit the red &lt;code&gt;has been blocked by CORS policy&lt;/code&gt; message in the console — even though the server responded just fine. The browser is the one refusing to hand the response over. This article walks through the same-origin policy that causes this, and what CORS (Cross-Origin Resource Sharing) actually does to relax it safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "origin" means
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: an origin is the combination of a URL's scheme (&lt;code&gt;https://&lt;/code&gt;), hostname (&lt;code&gt;wpmm.jp&lt;/code&gt;), and port. The path (&lt;code&gt;/blog/&lt;/code&gt;, etc.) doesn't count. &lt;code&gt;https://wpmm.jp&lt;/code&gt; and &lt;code&gt;https://en.wpmm.jp&lt;/code&gt; are different origins because the hostname differs; &lt;code&gt;https://wpmm.jp&lt;/code&gt; and &lt;code&gt;http://wpmm.jp&lt;/code&gt; are different origins too, because the scheme differs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Browsers enforce a baseline rule called the same-origin policy: JavaScript loaded from one origin generally cannot read the response of a request made to a different origin. This restriction lives in the browser itself — it has nothing to do with server configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the restriction exists at all
&lt;/h2&gt;

&lt;p&gt;The reasoning becomes obvious once you imagine a world without it. If the same-origin policy didn't exist, simply opening a malicious page in your browser would let its JavaScript quietly send a request to your bank's site in the background, using the authentication cookies already stored in your browser, and read the response — your account balance, for instance. The browser doesn't stop the request from being sent; what it stops is &lt;strong&gt;letting the page's JavaScript read the response that comes back from a different origin.&lt;/strong&gt; That single restriction closes off a large class of data-theft attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS is how a server safely loosens that restriction
&lt;/h2&gt;

&lt;p&gt;Plenty of legitimate use cases need to call an API on a different origin, though. CORS is the mechanism a server uses to explicitly declare "reads from this origin are allowed." When a server includes a header like &lt;code&gt;Access-Control-Allow-Origin: https://example.com&lt;/code&gt; in its response, the browser permits JavaScript running on that specific origin to read the response — and only that origin.&lt;/p&gt;

&lt;p&gt;The key point is that &lt;strong&gt;the decision is made by the browser, not the server.&lt;/strong&gt; The server is only offering a header; whether the response actually gets handed to the calling JavaScript is enforced client-side. A tool like curl or Postman, which isn't a browser, always sees the full response regardless of any CORS header — the restriction only kicks in for JavaScript running inside a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preflight requests
&lt;/h2&gt;

&lt;p&gt;For a "simple" GET request, the browser sends the real request first and then decides whether to expose the response based on the headers it gets back. But for a request that isn't considered simple — a POST with &lt;code&gt;Content-Type: application/json&lt;/code&gt;, for example — the browser sends an &lt;code&gt;OPTIONS&lt;/code&gt; request first, called a preflight, before it ever sends the real one. Only once the server responds to that preflight with headers like &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; and &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; does the browser go ahead and send the actual request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wildcard &lt;code&gt;*&lt;/code&gt; vs. a specific origin
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; can either allow any origin via the wildcard &lt;code&gt;*&lt;/code&gt;, or allow a single specific origin by name. The deciding factor is whether the API's response depends on credentials such as cookies or an &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A public, stateless endpoint that returns the same result no matter who calls it can safely use &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An endpoint whose response is tied to a cookie-based session, and therefore contains something personal to the caller, can't use &lt;code&gt;*&lt;/code&gt; — browsers won't allow &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt; to be paired with a wildcard origin. Instead, the server has to maintain an allowlist and match it against the incoming &lt;code&gt;Origin&lt;/code&gt; header on each request, echoing back only the origins it trusts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How our own backend splits this
&lt;/h2&gt;

&lt;p&gt;Two API endpoints behind our landing page happen to illustrate this split directly.&lt;/p&gt;

&lt;p&gt;The chatbot API (&lt;code&gt;server/wpmm-web/api/chat.php&lt;/code&gt;) checks the incoming &lt;code&gt;Origin&lt;/code&gt; header against an explicit allowlist and only echoes it back on a match:&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;$allowed_origins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'https://wpmm.jp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'https://www.wpmm.jp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'https://en.wpmm.jp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_ORIGIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowed_origins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Origin: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$origin&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;'Vary: Origin'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this endpoint tracks rate limits and conversation logs per session, letting any arbitrary site embed it would open the door to abuse. Restricting the allowed origins to our own landing pages keeps the set of places it can be called from under our control. The &lt;code&gt;Vary: Origin&lt;/code&gt; header alongside it is deliberate too — it tells any caching layer (a CDN, or the browser's own cache) that the response body depends on the &lt;code&gt;Origin&lt;/code&gt; header's value, so a response approved for one origin never gets served from cache to a request from a different one.&lt;/p&gt;

&lt;p&gt;The checkout-session API (&lt;code&gt;server/wpmm-web/api/checkout.php&lt;/code&gt;), on the other hand, returns &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;. It only creates a Stripe checkout session and hands back a redirect URL — nothing in that response is tied to the caller's cookies or identity — so there's little reason to restrict which origin can call it.&lt;/p&gt;

&lt;p&gt;There's a third pattern worth mentioning too: the "latest posts" section on our landing page (&lt;code&gt;includes/blog_latest.php&lt;/code&gt;) doesn't call the blog's REST API from browser JavaScript at all. Instead, the PHP running on the server fetches the REST API itself and renders the result as plain HTML. A server-to-server request like that never goes through a browser, so the same-origin policy and CORS simply don't apply to it. "Restrict which browser origins can call this directly" and "don't let the browser make the call in the first place" are two different answers to the same underlying problem.&lt;/p&gt;

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

&lt;p&gt;CORS is how a server safely relaxes the browser's default same-origin restriction by explicitly declaring which origins it trusts. The browser is the one enforcing the rule; the server is only stating a preference via a header. Whether to use a wildcard or an allowlist comes down to whether the response is tied to credentials. And it's worth remembering that CORS is a browser-specific concept in the first place — routing a call through the server instead of directly from client-side JavaScript sidesteps the whole question.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Environment Variables and .env Files: What Wins When They Conflict?</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 08 Oct 2026 23:34:06 +0000</pubDate>
      <link>https://dev.to/susumun/environment-variables-and-env-files-what-wins-when-they-conflict-5427</link>
      <guid>https://dev.to/susumun/environment-variables-and-env-files-what-wins-when-they-conflict-5427</guid>
      <description>&lt;p&gt;When building a Python application, configuration values tend to live in one of three places: real OS environment variables, a &lt;code&gt;.env&lt;/code&gt; file, or a hard-coded default in the code itself. These three can conflict, and when a value doesn't seem to be "taking effect," the cause is usually a misunderstanding of which one wins. This article walks through the resolution order between the three, and why it's designed the way it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a &lt;code&gt;.env&lt;/code&gt; file actually is
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: a &lt;code&gt;.env&lt;/code&gt; file is a plain text file listing &lt;code&gt;KEY=value&lt;/code&gt; pairs, one per line. It's used to keep environment-specific or sensitive values — API keys, database connection strings — out of the source code itself. It's almost always excluded from version control via &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Python, the go-to library is &lt;code&gt;python-dotenv&lt;/code&gt;. Calling &lt;code&gt;load_dotenv()&lt;/code&gt; reads the &lt;code&gt;.env&lt;/code&gt; file and populates &lt;code&gt;os.environ&lt;/code&gt; with its contents. From that point on, calling code just does &lt;code&gt;os.environ.get('API_KEY')&lt;/code&gt; without needing to know or care whether the value came from a real OS-level environment variable or from the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three places a value can come from, and their priority
&lt;/h2&gt;

&lt;p&gt;In general, a configuration value can exist at up to three levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OS environment variables&lt;/strong&gt; — values exported in the shell (&lt;code&gt;export API_KEY=...&lt;/code&gt;), or injected by CI systems and container runtimes at startup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; file&lt;/strong&gt; — values checked into the repository (or kept alongside it) that are loaded only in a developer's local environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard-coded defaults in the code&lt;/strong&gt; — the last resort, as in &lt;code&gt;os.environ.get('API_KEY', 'default-value')&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most dotenv-style libraries, &lt;code&gt;python-dotenv&lt;/code&gt; included, default to a specific rule here: &lt;strong&gt;a value that's already set as a real OS environment variable is not overwritten by the &lt;code&gt;.env&lt;/code&gt; file.&lt;/strong&gt; Unless you explicitly pass &lt;code&gt;override=True&lt;/code&gt;, &lt;code&gt;load_dotenv()&lt;/code&gt; respects whatever is already present in &lt;code&gt;os.environ&lt;/code&gt; and simply skips any matching key found in the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;That gives a priority order of: &lt;strong&gt;OS environment variable &amp;gt; &lt;code&gt;.env&lt;/code&gt; file &amp;gt; hard-coded default.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this ordering makes sense
&lt;/h2&gt;

&lt;p&gt;At first glance it might seem more intuitive for the value loaded last (the &lt;code&gt;.env&lt;/code&gt; file) to win. But there's a good reason OS environment variables take precedence instead.&lt;/p&gt;

&lt;p&gt;The classic case is CI pipelines and containerized deployments. In those environments, secrets management systems — GitHub Actions secrets, a &lt;code&gt;docker run -e&lt;/code&gt; flag, and similar mechanisms — inject production-relevant or CI-specific values directly as OS environment variables. If a &lt;code&gt;.env&lt;/code&gt; file checked into the repo (typically containing dummy values or a developer's personal local settings) were allowed to silently overwrite those injected values, tests and deployments would end up running against the wrong configuration without any obvious error.&lt;/p&gt;

&lt;p&gt;The underlying principle — &lt;strong&gt;a value that has already been explicitly and deliberately set should not be silently overwritten by a file loaded later&lt;/strong&gt; — is a common pattern in configuration resolution design generally. Ordering things so that "the more intentionally a value was set, the higher its priority" lets both cases work correctly at once: a local developer's &lt;code&gt;.env&lt;/code&gt; file is used whenever no OS environment variable overrides it, while CI and production environments reliably use whatever the infrastructure layer injected, regardless of what happens to be sitting in the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  A similar pattern in our own codebase: explicit setting &amp;gt; auto-detection &amp;gt; hard-coded fallback
&lt;/h2&gt;

&lt;p&gt;This application doesn't use &lt;code&gt;.env&lt;/code&gt; files, but a structurally similar multi-tier fallback shows up in &lt;code&gt;core/log_i18n.py&lt;/code&gt;'s automatic UI-language detection (&lt;code&gt;get_ui_language()&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="c1"&gt;# Resolution order (first valid value wins):
#   1. Environment variables LC_ALL / LC_MESSAGES / LANG (POSIX standard)
#   2. Windows API: GetUserDefaultUILanguage
#   3. locale.getlocale() / locale.getdefaultlocale() (legacy fallback)
#   4. If nothing resolves -&amp;gt; 'en' (defaults toward international users)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, the caller of this function checks for an explicit language setting in &lt;code&gt;settings.json&lt;/code&gt; first, and skips the automatic detection entirely if one is present.&lt;/p&gt;

&lt;p&gt;Laid out as a priority chain, that's: &lt;strong&gt;an explicit user choice (&lt;code&gt;settings.json&lt;/code&gt;) &amp;gt; information the OS/shell already exposes (&lt;code&gt;LANG&lt;/code&gt; and friends) &amp;gt; a guess from the standard library (the &lt;code&gt;locale&lt;/code&gt; module) &amp;gt; a fixed fallback (&lt;code&gt;'en'&lt;/code&gt;) when nothing else resolves.&lt;/strong&gt; It's the same underlying idea as the &lt;code&gt;.env&lt;/code&gt; priority rule, applied to a different problem: a deliberately configured value should always outrank an automatically detected one, and there should always be a safe, hard-coded value at the very bottom so the system never has no answer at all.&lt;/p&gt;

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

&lt;p&gt;The typical resolution order for configuration values — OS environment variable, then &lt;code&gt;.env&lt;/code&gt; file, then hard-coded default — exists specifically to stop a developer's local &lt;code&gt;.env&lt;/code&gt; file from accidentally overriding values that CI or container infrastructure deliberately injected. That same idea, "prefer the more intentional value and always keep a safe fallback underneath," generalizes well beyond &lt;code&gt;.env&lt;/code&gt; files to configuration resolution in general.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Python's venv Works — Why It Keeps Projects From Fighting Over the System Python</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 08 Oct 2026 00:39:18 +0000</pubDate>
      <link>https://dev.to/susumun/how-pythons-venv-works-why-it-keeps-projects-from-fighting-over-the-system-python-h95</link>
      <guid>https://dev.to/susumun/how-pythons-venv-works-why-it-keeps-projects-from-fighting-over-the-system-python-h95</guid>
      <description>&lt;p&gt;Anyone who has worked with Python tooling has run into "virtual environments" (venv) sooner or later. A single command, &lt;code&gt;python3 -m venv .venv&lt;/code&gt;, creates a directory that most Python projects treat as a given. What is that directory actually doing, and why has it become such a standard part of the workflow? This post looks at the problem venv solves and how it works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Projects Sharing One Python Installation
&lt;/h2&gt;

&lt;p&gt;Working on multiple Python projects on the same machine eventually runs into a conflict: one project needs version 2 of a library, another needs version 3 of the same library. Running &lt;code&gt;pip install&lt;/code&gt; directly against the system Python installs into one shared &lt;code&gt;site-packages&lt;/code&gt; directory for the entire machine, so installing the second project's dependencies overwrites the first project's, and one of them breaks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;site-packages&lt;/code&gt; is the directory where third-party libraries installed via &lt;code&gt;pip install&lt;/code&gt; actually live. Python looks inside it whenever code does an &lt;code&gt;import&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On some operating systems, the system Python is also used internally by OS-level tooling (package managers, for instance), so carelessly installing or upgrading libraries into that shared location risks affecting the OS itself, not just your own projects. A virtual environment sidesteps this by giving each project its own independent &lt;code&gt;site-packages&lt;/code&gt;, rather than everyone sharing one pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happens Inside a venv
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;python3 -m venv .venv&lt;/code&gt; creates a directory roughly structured like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.venv/
├── bin/                       # python3/pip executables (symlink or copy)
├── lib/.../site-packages/     # this project's own library directory
├── pyvenv.cfg                 # records where the base system Python lives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail is that &lt;code&gt;.venv/bin/python3&lt;/code&gt; isn't a separate Python implementation — it's the same interpreter as the system Python, either copied or symlinked. What changes is where it looks for libraries. When &lt;code&gt;.venv/bin/python3&lt;/code&gt; starts up, it reads &lt;code&gt;pyvenv.cfg&lt;/code&gt; and redirects its &lt;code&gt;site-packages&lt;/code&gt; search path to &lt;code&gt;.venv/lib/.../site-packages&lt;/code&gt; instead of the system-wide location. Any &lt;code&gt;pip install&lt;/code&gt; run through that interpreter installs into this project-local directory, leaving the system Python's environment untouched.&lt;/p&gt;

&lt;p&gt;The familiar &lt;code&gt;source .venv/bin/activate&lt;/code&gt; command isn't the mechanism that provides this isolation — it's a convenience shell script layered on top. Running it just prepends &lt;code&gt;.venv/bin&lt;/code&gt; to the &lt;code&gt;PATH&lt;/code&gt; environment variable and sets a &lt;code&gt;VIRTUAL_ENV&lt;/code&gt; variable, so that typing &lt;code&gt;python&lt;/code&gt; or &lt;code&gt;pip&lt;/code&gt; in the terminal afterward resolves to the venv's copies first. The isolation itself doesn't require activation at all: calling &lt;code&gt;.venv/bin/python3 script.py&lt;/code&gt; by its full path gets you the same isolated environment, activated or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Example: Startup-Time venv Management in &lt;code&gt;_launcher.sh&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This app's Mac/Linux launch script, &lt;code&gt;_launcher.sh&lt;/code&gt;, relies on exactly that direct, activation-free call for a health check:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;".venv/bin/python3"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;NEEDS_SETUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STAMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nv"&gt;$STAMP&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PY_VER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQ_HASH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;NEEDS_SETUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; .venv/bin/python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys; sys.exit(0)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;NEEDS_SETUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than rebuilding &lt;code&gt;.venv&lt;/code&gt; on every launch, the script keeps a stamp file at &lt;code&gt;.venv/.install_stamp&lt;/code&gt; containing three values as a single string: the machine architecture (&lt;code&gt;uname -m&lt;/code&gt;), the Python version, and a hash of &lt;code&gt;requirements.txt&lt;/code&gt;. On the next launch, it compares this composite key against the stored one. If they match, &lt;code&gt;.venv&lt;/code&gt; is assumed unchanged and reused as-is — the script just runs &lt;code&gt;source .venv/bin/activate&lt;/code&gt; and moves on. If any of the three has changed (a dependency bump shipped in an app update, or the machine changing from Intel to Apple Silicon, for example), &lt;code&gt;.venv&lt;/code&gt; is deleted entirely and rebuilt from scratch with &lt;code&gt;python3 -m venv .venv&lt;/code&gt; followed by &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The line &lt;code&gt;.venv/bin/python3 -c "import sys; sys.exit(0)"&lt;/code&gt; looks like it does nothing, but it's a health check confirming the venv's Python binary can actually run. This can fail, for example, after an OS update moves the system Python's location out from under a symlink the venv still points to — in which case a rebuild is triggered as well.&lt;/p&gt;

&lt;p&gt;This "don't rebuild unless the requirements.txt hash changes" design carries a hidden assumption worth naming. As covered in &lt;a href="https://en.wpmm.jp/blog/pip-version-specifiers-requirements-txt/" rel="noopener noreferrer"&gt;our post on pip's version-specifier syntax&lt;/a&gt;, this app's own &lt;code&gt;requirements.txt&lt;/code&gt; uses &lt;code&gt;&amp;gt;=&lt;/code&gt; lower-bound specifiers (like &lt;code&gt;flask&amp;gt;=3.0.0&lt;/code&gt;) rather than &lt;code&gt;==&lt;/code&gt; exact pins. That means the &lt;em&gt;file's&lt;/em&gt; hash can stay identical across two separate &lt;code&gt;pip install&lt;/code&gt; runs, while the &lt;em&gt;actual&lt;/em&gt; library versions installed can still differ depending on when the install happened. A matching hash guarantees the venv was built from the same &lt;code&gt;requirements.txt&lt;/code&gt;, not that its installed contents are byte-for-byte identical every time — a distinction worth keeping in mind.&lt;/p&gt;

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

&lt;p&gt;A venv solves the problem of multiple projects fighting over one shared Python installation by giving each project its own isolated &lt;code&gt;site-packages&lt;/code&gt;. &lt;code&gt;activate&lt;/code&gt; is a convenience layer that temporarily rewrites &lt;code&gt;PATH&lt;/code&gt;; the actual isolation works by simply calling &lt;code&gt;.venv/bin/python3&lt;/code&gt; directly. This app's launcher script leans on that property, checking for environment drift (architecture, Python version, dependency changes) via a hash-based stamp file instead of rebuilding the environment on every single launch — while still catching the cases where it genuinely needs to.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Image Optimization and WebP Basics — Why WebP Is Smaller, and How Browsers Decide to Use It</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 06 Oct 2026 23:20:40 +0000</pubDate>
      <link>https://dev.to/susumun/image-optimization-and-webp-basics-why-webp-is-smaller-and-how-browsers-decide-to-use-it-2mfe</link>
      <guid>https://dev.to/susumun/image-optimization-and-webp-basics-why-webp-is-smaller-and-how-browsers-decide-to-use-it-2mfe</guid>
      <description>&lt;p&gt;It's often cited that the biggest drag on WordPress page speed isn't code at all — it's images. The same-looking photo can end up dramatically smaller in file size depending on whether it stays as JPEG/PNG or gets converted to WebP. This post looks at why WebP tends to be lighter, how browsers negotiate which format to load, and how WordPress itself has adopted the format over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WebP Is Smaller: A Different Compression Approach
&lt;/h2&gt;

&lt;p&gt;JPEG, standardized back in 1992, is built on discrete cosine transform (DCT) compression. WebP, introduced by Google in 2010, takes a different route: it repurposes the intra-frame compression techniques from the VP8 video codec for still images. Because it inherits prediction-based encoding refined for video, WebP can often hit a smaller file size than JPEG at a comparable level of visible quality loss.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: intra-frame compression means compressing a single video frame using only the information within that frame, without referencing neighboring frames. That's essentially the same problem as compressing one still image, which is why video codec techniques transfer so directly to image formats.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another advantage of WebP is that it covers multiple use cases under one format. It supports lossy compression for photos, but also lossless compression for things like logos and screenshots, plus alpha-channel transparency (like PNG) and even simple animation (like GIF). Formats that used to require picking JPEG for photos, PNG for transparency, and GIF for animation can now be handled by WebP alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Browsers Decide Which Format to Serve
&lt;/h2&gt;

&lt;p&gt;When WebP was new, browser support was inconsistent, and HTTP content negotiation helped bridge that gap. A browser requesting an image sends a header like &lt;code&gt;Accept: image/webp,image/*,*/*&lt;/code&gt;, and a server or CDN can inspect it to decide: if the browser says it understands WebP, serve the lighter WebP version; otherwise, fall back to JPEG or PNG.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: content negotiation is the general term for a server returning different content at the same URL depending on what format the client claims to support. The same idea shows up for language (&lt;code&gt;Accept-Language&lt;/code&gt;) and compression (&lt;code&gt;Accept-Encoding&lt;/code&gt;), not just image formats.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same idea can be expressed explicitly in HTML. Wrapping a &lt;code&gt;&amp;lt;source type="image/webp" srcset="photo.webp"&amp;gt;&lt;/code&gt; element inside a &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; tag, followed by a regular &lt;code&gt;&amp;lt;img src="photo.jpg"&amp;gt;&lt;/code&gt; fallback, lets the browser pick whichever format it understands first — WebP if supported, the JPEG &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; otherwise. This is convenient because the browser handles the decision itself; no server-side negotiation logic is required.&lt;/p&gt;

&lt;p&gt;All major browsers today (Chrome, Firefox, Edge, Safari) support WebP, so the "unsupported" branch of that logic rarely gets exercised in practice. Still, some older email-preview renderers and certain image-processing bots don't support it, so keeping a fallback in place remains a reasonable default.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebP Support in WordPress Core
&lt;/h2&gt;

&lt;p&gt;For a long time, WordPress restricted uploadable image formats to JPEG, PNG, GIF, and a few others — trying to upload a &lt;code&gt;.webp&lt;/code&gt; file to the media library used to fail with an error about the file type not being allowed for security reasons. WebP was officially added to the allowed MIME type list (&lt;code&gt;wp_get_mime_types()&lt;/code&gt;) starting with WordPress 5.8, after which WebP files could be uploaded directly to the media library.&lt;/p&gt;

&lt;p&gt;Later versions of WordPress went a step further: derivative sizes generated from an uploaded JPEG (the various sizes registered via &lt;code&gt;add_image_size()&lt;/code&gt;, such as thumbnails) can now be automatically converted to WebP and stored alongside the original. The original JPEG file itself is left untouched, while the smaller sizes used in post listings and featured images become WebP — reducing transferred bytes on the display side while preserving compatibility of the source file. Whether this conversion actually happens depends on the server's image-processing library (GD or Imagick) supporting WebP output, so it isn't guaranteed on every hosting environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Similar Case in Our Own Codebase: Mapping Extensions to MIME Types
&lt;/h2&gt;

&lt;p&gt;This app's white-label reporting feature (available on the STD/BIZ plans) touches WebP in a comparable way. The agency-logo upload endpoint (&lt;code&gt;site_manager_web.py::upload_report_logo&lt;/code&gt;) includes &lt;code&gt;.webp&lt;/code&gt; in its list of allowed extensions, and the endpoint that serves the logo back for preview (&lt;code&gt;report_logo_preview&lt;/code&gt;) maintains a lookup table from extension to MIME type:&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;mime_map&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;.png&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;image/png&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;.jpg&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;image/jpeg&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;.jpeg&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;image/jpeg&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;.gif&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;image/gif&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;.webp&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;image/webp&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;.svg&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;image/svg+xml&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;Similarly, the routine that embeds site screenshots into a report PDF (&lt;code&gt;report_generator.py::_encode_image_b64&lt;/code&gt;) determines the correct MIME type from the file extension (mapping &lt;code&gt;webp&lt;/code&gt; to &lt;code&gt;image/webp&lt;/code&gt;) before encoding the file as a base64 data URI.&lt;/p&gt;

&lt;p&gt;Why bother with this lookup table at all? Browsers and PDF renderers decide how to interpret an image based on the actual &lt;code&gt;Content-Type&lt;/code&gt; header sent (or the MIME type declared inside a data URI) — not the file extension itself. If a &lt;code&gt;.webp&lt;/code&gt; file were mistakenly served with a MIME type like &lt;code&gt;image/png&lt;/code&gt;, the browser might fail to render it, or offer a download dialog instead of displaying it inline. Explicitly mapping each extension to its correct MIME type is a small, unglamorous step, but it's the kind of detail that prevents broken image rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the OGP Image Generator Still Outputs PNG, Not WebP
&lt;/h2&gt;

&lt;p&gt;On the other hand, this blog's dynamic OGP image generator — &lt;code&gt;ogp-generator.php&lt;/code&gt;, covered previously in &lt;a href="https://en.wpmm.jp/blog/cache-control-vs-etag-what-they-control/" rel="noopener noreferrer"&gt;our Cache-Control vs. ETag post&lt;/a&gt; — deliberately outputs PNG rather than WebP. The reason: social media crawlers from platforms like X (formerly Twitter) and Facebook are separate programs from a person's browser, and they don't necessarily track the same level of image-format support as the latest browser releases. Since an OGP image's job is to be read by a social platform's crawler rather than a human's browser, choosing the most broadly compatible format — PNG or JPEG — reduces the risk of a share card failing to render correctly.&lt;/p&gt;

&lt;p&gt;Even with the same underlying goal of keeping images small, the right format can differ depending on whether the audience is a general-purpose browser or a social crawler.&lt;/p&gt;

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

&lt;p&gt;WebP borrows video-codec compression techniques to deliver photos, transparent images, and simple animations in one format, typically at a smaller file size than the JPEG/PNG/GIF combination it replaces. Browsers can negotiate format support through the &lt;code&gt;Accept&lt;/code&gt; header or declare it explicitly with the &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element, and WordPress core has gradually adopted WebP — first as an upload-allowed format in version 5.8, later with automatic conversion for generated image sizes. Lighter isn't always better, though: when compatibility with a specific consumer (like a social media crawler) matters more than raw file size, sticking with an older, more universally supported format can be the right call.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cookie and Session Authentication Basics — What Happens Behind a wp-admin Login</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 06 Oct 2026 00:13:34 +0000</pubDate>
      <link>https://dev.to/susumun/cookie-and-session-authentication-basics-what-happens-behind-a-wp-admin-login-4ii4</link>
      <guid>https://dev.to/susumun/cookie-and-session-authentication-basics-what-happens-behind-a-wp-admin-login-4ii4</guid>
      <description>&lt;p&gt;The &lt;a href="https://en.wpmm.jp/blog/wordpress-rest-api-cookie-nonce-authentication/" rel="noopener noreferrer"&gt;previous article&lt;/a&gt; covered how REST API requests inside wp-admin are protected by a combination of cookies and nonces. This time we go one level deeper: what actually happens when you "log in," and why does entering a password once keep you authenticated across dozens of page loads afterward? We'll also look at how WordPress's approach differs from the more common server-side session model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens the moment you click "Log In"
&lt;/h2&gt;

&lt;p&gt;When you submit the wp-admin login form, &lt;code&gt;wp_signon()&lt;/code&gt; first checks the submitted password against the hashed password stored in the database — nothing unusual so far. Once authentication succeeds, &lt;code&gt;wp_set_auth_cookie()&lt;/code&gt; issues an authentication cookie to the browser. From then on, the browser automatically attaches this cookie to every subsequent request, so you never have to re-enter your password.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: HTTP itself has no concept of "state." The server doesn't remember, at the time of the next request, who logged in during the previous one. The general term for solving this problem — maintaining a logged-in state on top of a stateless protocol — is "session management."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's actually inside the authentication cookie
&lt;/h2&gt;

&lt;p&gt;WordPress's authentication cookie (stored under a name like &lt;code&gt;wordpress_logged_in_*&lt;/code&gt;) is a pipe-delimited string. Roughly speaking, it consists of four parts: the username, an expiration timestamp, a session token, and an HMAC signature. The important detail here is that the username and expiration are stored in something close to plain text inside the cookie itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: HMAC (Hash-based Message Authentication Code) is a tamper-detection signature computed with a secret key. The same input and key always produce the same output, but a third party who doesn't know the key cannot forge a valid signature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If a user could freely rewrite the contents of that cookie, they could swap in someone else's username and impersonate them. The HMAC signature at the end prevents exactly that. The server computes this signature using secret strings defined in &lt;code&gt;wp-config.php&lt;/code&gt; — &lt;code&gt;AUTH_KEY&lt;/code&gt; / &lt;code&gt;AUTH_SALT&lt;/code&gt; (and a separate &lt;code&gt;LOGGED_IN_KEY&lt;/code&gt; / &lt;code&gt;LOGGED_IN_SALT&lt;/code&gt; pair specifically for the login cookie). On every request, the server redoes this same computation and checks whether the result matches the signature in the cookie. Without knowing the secret key, an attacker who tampers with the username or expiration cannot produce a matching signature, so the forgery is caught immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification that runs on every single request
&lt;/h2&gt;

&lt;p&gt;Every time a page loads, WordPress runs this check through &lt;code&gt;wp_validate_auth_cookie()&lt;/code&gt;. The flow is: split the cookie on the pipe character, confirm the expiration hasn't passed, recompute the expected HMAC from the username, token, and expiration, and compare it against the signature stored in the cookie. If even one of these checks fails, the request is treated as unauthenticated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The apparent contradiction: "stateless," yet "log out everywhere" works
&lt;/h2&gt;

&lt;p&gt;Based on the description so far, WordPress's authentication scheme looks entirely self-contained — as long as the username, expiration, and signature can be verified, the server doesn't need to look anything up in a database on every request. This has a real advantage: the server doesn't need to hold onto a large volume of session data.&lt;/p&gt;

&lt;p&gt;And yet, the WordPress admin dashboard has a "log out everywhere" feature. If the cookie were truly self-contained with nothing on the server side, there would be no way to invalidate an already-issued, cryptographically valid cookie from the outside. The piece that resolves this is the "session token" embedded in the cookie. WordPress separately maintains, per user, a list of currently valid session tokens as user metadata in the database — handled by a class called &lt;code&gt;WP_Session_Tokens&lt;/code&gt;. When validating the authentication cookie, WordPress checks not only that the HMAC signature matches, but also that the token inside the cookie still appears in that user's list of valid tokens. Triggering "log out everywhere" clears that list of tokens server-side; even though the signature itself would still verify correctly, the associated token can no longer be found, so all subsequent requests using that cookie are treated as expired.&lt;/p&gt;

&lt;p&gt;In other words, WordPress's authentication design is a hybrid of a purely self-contained token and a server-managed, revocable session record. The signature check happens fast and self-contained, while only the part that genuinely needs revocation — the token list — depends on server-side state, getting the benefits of both approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this compares to the traditional server-side session model
&lt;/h2&gt;

&lt;p&gt;Many web applications take the opposite approach. The cookie handed to the browser contains nothing meaningful at all — just a random session ID — while the actual data (username, permissions, and so on) lives entirely in a server-side lookup table mapping session IDs to user information, held in memory or a database. This means the server must look up that table on every request, but revoking a session is as simple as deleting a single entry from it.&lt;/p&gt;

&lt;p&gt;It helps to think of WordPress's authentication cookie as sitting between these two designs. Lightweight data like the username and expiration travels inside the signed cookie itself, keeping verification cheap, while only the part that actually needs revocation logic — which tokens are currently valid — is carved out into server-side user metadata.&lt;/p&gt;

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

&lt;p&gt;A wp-admin login state is re-verified on every request through an authentication cookie made up of four parts: username, expiration, token, and HMAC signature. Looking only at the signature check, it resembles a fully self-contained token scheme, but it's actually a hybrid combined with a list of valid session tokens stored in user metadata — which is precisely what makes a server-initiated action like "log out everywhere" possible. The nonce verification covered in the &lt;a href="https://en.wpmm.jp/blog/wordpress-rest-api-cookie-nonce-authentication/" rel="noopener noreferrer"&gt;previous article on REST API authentication&lt;/a&gt; also depends on this login cookie working correctly in the first place; both mechanisms ultimately rest on the same underlying authentication layer.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>REST API Authentication: Cookie+Nonce vs. Application Passwords</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 00:48:52 +0000</pubDate>
      <link>https://dev.to/susumun/rest-api-authentication-cookienonce-vs-application-passwords-3j36</link>
      <guid>https://dev.to/susumun/rest-api-authentication-cookienonce-vs-application-passwords-3j36</guid>
      <description>&lt;p&gt;WordPress's REST API (covered in &lt;a href="https://en.wpmm.jp/blog/wordpress-rest-api-wp-json-basics/" rel="noopener noreferrer"&gt;an earlier article&lt;/a&gt;) mixes two kinds of endpoints: some that anyone can read without authenticating, and others that reject you outright unless you're logged in. Inside wp-admin, the browser is quietly firing off requests to that same API the whole time you're editing — and yet you never re-enter your password for each one. This article looks at the mechanism behind that: cookie+nonce authentication, and a separate mechanism built for a different purpose, Application Passwords.&lt;/p&gt;

&lt;h2&gt;
  
  
  What authenticates a request inside wp-admin
&lt;/h2&gt;

&lt;p&gt;When you log into WordPress, your browser receives an authentication cookie named &lt;code&gt;wordpress_logged_in_*&lt;/code&gt;. From then on, every request your browser sends to that same domain carries this cookie automatically, so the server can identify "who's logged in" just by checking it. When wp-admin's own JavaScript calls the REST API — auto-saving a draft, or the block editor updating a post in the background — it's riding on that same cookie.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: CSRF (Cross-Site Request Forgery) is an attack where, while you're logged into a site, a malicious page you happen to open triggers requests to that site using your browser's cookies without your intent — deleting a post or changing a setting, for example. Because browsers attach cookies automatically, a cookie alone can't prove the request was something you actually meant to do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is exactly the gap CSRF exploits. Since cookies are sent automatically, visiting a booby-trapped page while logged in would carry the same cookie along with it. If the REST API accepted write requests based on cookie validation alone, simply loading a malicious page while logged in could trigger an unwanted post deletion or account change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the nonce comes in
&lt;/h2&gt;

&lt;p&gt;A nonce closes that gap. Despite the name suggesting "used once," WordPress's nonce is actually a token tied to the current login session, valid for a fixed window (24 hours by default, extendable to roughly 48 hours with rotation), and scoped to a specific action. The server generates one with &lt;code&gt;wp_create_nonce('some-action-name')&lt;/code&gt; and embeds it in the wp-admin page's HTML/JS. Write requests to the REST API are expected to carry this value in an &lt;code&gt;X-WP-Nonce&lt;/code&gt; header, and the server checks it against the logged-in user with &lt;code&gt;wp_verify_nonce()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A malicious external site has no way to obtain that nonce value — it's only embedded in the wp-admin page itself, which lives on the same origin. So even if an attacker's page manages to reuse your stolen cookie, the nonce check still blocks it. Put simply: the cookie answers "who is this," and the nonce answers "did this person actually intend this action from this page, right now." Both have to check out before a wp-admin REST request goes through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where cookie+nonce falls short
&lt;/h2&gt;

&lt;p&gt;This whole design assumes requests originate from the same browser, inside the same wp-admin session. The flip side is that it doesn't work well for programs that never touch a browser — a command-line tool, or a script running on a different server, that wants to call the REST API directly. Cookies are tied to a browser's login session, and there's no natural way for an external program to acquire or maintain one; nonces are similarly short-lived and need to be re-fetched every time a wp-admin page loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Passwords: a separate door in
&lt;/h2&gt;

&lt;p&gt;Application Passwords, shipped in WordPress core since version 5.6 (2020), answers exactly this need — a way for external programs to call the REST API safely — through a mechanism entirely separate from cookie+nonce. Under the hood it resembles HTTP Basic authentication. From their profile page in wp-admin, a user can generate a new Application Password by naming the application it's for. What comes out is a randomly generated password distinct from the login password; sent in an &lt;code&gt;Authorization: Basic&lt;/code&gt; header, it carries the same permissions as the account.&lt;/p&gt;

&lt;p&gt;The key differences from cookie+nonce are twofold. First, it doesn't depend on any browser session state — once you have the password, any program can use it immediately. Second, each one can be revoked individually. There's no need to hand your actual login password to an external integration; if you decide to stop using that integration, you revoke just that one Application Password from wp-admin, without touching your login credentials or affecting any other connected tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real case where no authentication is needed at all
&lt;/h2&gt;

&lt;p&gt;Interestingly, the "From the Blog" section on our own LP (&lt;code&gt;en.wpmm.jp/&lt;/code&gt;) — built by the &lt;code&gt;blog_latest.php&lt;/code&gt; script covered in &lt;a href="https://en.wpmm.jp/blog/wordpress-rest-api-wp-json-basics/" rel="noopener noreferrer"&gt;the earlier article&lt;/a&gt; — uses neither of these mechanisms. It only calls &lt;code&gt;/wp/v2/posts&lt;/code&gt;, a read-only endpoint for published posts, which is designed to be accessible to anyone without authentication in the first place. REST API authentication only becomes necessary once you go beyond public data — fetching drafts, creating or updating posts, changing settings. Thinking of it in three tiers — anyone can read, only the logged-in user can write, and an external program writes through a dedicated Application Password — makes it straightforward to decide which authentication approach a given task actually needs.&lt;/p&gt;

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

&lt;p&gt;Requests inside wp-admin combine two checks — a cookie (who's logged in) and a nonce (did this person intend this action, from this page, right now) — to block CSRF while still letting you skip re-entering your password on every request. But because that design leans heavily on browser session state, it doesn't extend naturally to external programs calling the REST API. Application Passwords fill that gap with a separate class of token that can be issued and revoked independently of the login password. The same phrase, "REST API authentication," calls for a different mechanism depending on whether the request originates inside a browser or outside one — worth keeping in mind whenever you're deciding how a piece of code should authenticate.&lt;/p&gt;

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