<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>SHIFT64 Blog</title><description>Technical articles, tutorials, and insights about web development, WordPress, WooCommerce, and performance optimization.</description><link>https://shift64.com</link><language>en</language><item><title>I Bought the Domain Before I Ran the Test. EmDash Still Lost to WordPress.</title><link>https://shift64.com/blog/emdash-cms-vs-wordpress-honest-benchmark</link><guid isPermaLink="true">https://shift64.com/blog/emdash-cms-vs-wordpress-honest-benchmark</guid><description>I read 150 EmDash hot takes in a week. Almost none came with data. So I built two identical sites, ran 4,732 cold-isolated curl measurements over ~3.5 days, and asked an SEO crawl-stats expert to grade the result blind. Here is what actually happened.</description><pubDate>Wed, 15 Apr 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;I built &lt;strong&gt;two parallel content sites&lt;/strong&gt; with identical posts: one on &lt;strong&gt;EmDash + Cloudflare Workers + D1&lt;/strong&gt; (&lt;a href=&quot;https://emdashcms.pl&quot;&gt;emdashcms.pl&lt;/a&gt;), one on &lt;strong&gt;a hand-coded WordPress theme on Hetzner&lt;/strong&gt; (&lt;a href=&quot;https://emdash.pl&quot;&gt;emdash.pl&lt;/a&gt;). Both sites scaffolded with Claude Code.&lt;/li&gt;
&lt;li&gt;I ran &lt;strong&gt;4,732 cold-isolated curl measurements&lt;/strong&gt; over &lt;strong&gt;~3.5 days&lt;/strong&gt; (182 runs × 13 pages × 2 sites), plus follow-up runs after every optimization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WordPress was 6.4× faster&lt;/strong&gt; on server processing (84 ms vs 543 ms mean). After I rewrote EmDash’s data fetching with &lt;code&gt;server:defer&lt;/code&gt; and a batch tag API, the gap narrowed to &lt;strong&gt;4.1×&lt;/strong&gt; — still 322 ms vs 78 ms.&lt;/li&gt;
&lt;li&gt;The Cloudflare &lt;strong&gt;paid Workers plan ($5/mo) made things slightly worse&lt;/strong&gt;, not better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jacek Żmudziński&lt;/strong&gt;, who reviews Search Console crawl stats for a living, looked at the download-time numbers blind. He filed ~1,300 ms in the “scrap and rebuild” tier. The same blind review put well-tuned WordPress in “decent” and Astro in “exemplary”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;After one week of indexing&lt;/strong&gt;, the WordPress site has &lt;strong&gt;4.3× more clicks&lt;/strong&gt;, &lt;strong&gt;3.9× higher CTR&lt;/strong&gt;, and outranks the EmDash showcase site on the EmDash brand query itself (position 3.4 vs 13.7). And the WP site received zero updates the entire time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I wanted EmDash to win.&lt;/strong&gt; I was so sure it would that I bought &lt;code&gt;emdashcms.pl&lt;/code&gt; before the first measurement. This article exists because the data did not cooperate.&lt;/li&gt;
&lt;li&gt;Both repos (EmDash site and the WP comparison theme) are public — linked at the bottom.&lt;/li&gt;
&lt;li&gt;In the meantime: come argue with me &lt;a href=&quot;https://x.com/matisportowiec/status/2044370462029869129&quot;&gt;on X&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/posts/mateusz-zadorozny_i-read-150-emdash-hot-takes-in-a-week-maybe-activity-7450135553863274496-PfCM&quot;&gt;on LinkedIn&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;i-read-150-emdash-takes-in-a-week-maybe-1-in-100-had-installed-it&quot;&gt;I read 150 EmDash takes in a week. Maybe 1 in 100 had installed it.&lt;/h2&gt;
&lt;p&gt;When EmDash launched, my feed turned into a wall-to-wall opinion war.&lt;/p&gt;
&lt;p&gt;One side declared EmDash the final nail in WordPress’s coffin. The other side wrote it off as a toy that would never matter. And reading both camps for a week, I had the same dull feeling over and over: &lt;strong&gt;almost nobody had actually installed the thing.&lt;/strong&gt; Maybe 1 post in 100 read like the author had genuinely touched the product. Not the loudest enthusiasts. Not the loudest skeptics. The other 99 were copy-pasting whichever side’s bullet points sounded better and going to bed. A handful had a measured, sober take. The vast majority were retransmitting marketing.&lt;/p&gt;
&lt;p&gt;I was firmly in the enthusiast camp. Not “WordPress is dead” enthusiast — I run a &lt;a href=&quot;https://shift64.com&quot;&gt;WooCommerce performance practice&lt;/a&gt;, I know how stubbornly that ecosystem keeps shipping value — but enthusiast in the “this is exactly the missing piece for the small-business segment” sense.&lt;/p&gt;
&lt;p&gt;So I did what enthusiasts do. I bought the domain — &lt;code&gt;emdashcms.pl&lt;/code&gt; — before I had any measurements. I scaffolded the site in an evening with Claude Code. I was already drafting the migration playbook in my head.&lt;/p&gt;
&lt;p&gt;Then I started actually using the site I’d just built.&lt;/p&gt;
&lt;p&gt;I keep the &lt;a href=&quot;https://chromewebstore.google.com/detail/core-web-vitals-test/aaldphpndekiaclbcmfgbghngcmeeeaf&quot;&gt;Core Web Vitals Test&lt;/a&gt; Chrome extension pinned to my toolbar — the kind that sits there and quietly reports LCP, INP and CLS for whatever page I’m on, in real time. On most well-built sites I forget it exists. On a healthy page it does nothing. The icon turns red the moment a metric crosses the “poor” threshold.&lt;/p&gt;
&lt;p&gt;Clicking through my own EmDash post pages, that little icon started blinking red. Not catastrophically. Not on every page. Just often enough that I couldn’t pretend I hadn’t seen it. The page I had built specifically to &lt;em&gt;prove&lt;/em&gt; how fast EmDash was — was the page lighting up the warning lamp.&lt;/p&gt;
&lt;p&gt;That was the moment. I closed the tab, opened a terminal, and started writing the benchmark this article is built on.&lt;/p&gt;
&lt;h2 id=&quot;what-i-was-hoping-emdash-would-unlock&quot;&gt;What I was hoping EmDash would unlock&lt;/h2&gt;
&lt;p&gt;This matters, because the disappointment only makes sense if you understand the expectation.&lt;/p&gt;
&lt;p&gt;A huge slice of the SMB market sits on WordPress for one reason: &lt;strong&gt;a non-technical owner needs to edit the offer, the contact details, the team page, a service description, without filing a ticket&lt;/strong&gt;. That’s it. They don’t need WooCommerce. They don’t need 47 plugins. They need a CMS.&lt;/p&gt;
&lt;p&gt;What they get instead is the WordPress maintenance treadmill: plugin updates, security patches, the occasional surprise white screen, an annual “your site is slow” panic. Marketers and small-business owners I talk to are tired of it.&lt;/p&gt;
&lt;p&gt;EmDash, on paper, is the answer to that complaint:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Astro for the runtime&lt;/strong&gt; — zero-JS by default, edge-deployable, the kind of speed I’d brag about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A real admin panel&lt;/strong&gt; — so the client can edit content without touching code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Workers + D1&lt;/strong&gt; — serverless, no server to patch, no PHP version to worry about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare’s blessing&lt;/strong&gt; — they shipped migration tooling on day one, which sent a strong “this is going somewhere” signal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If that combination delivered WordPress-grade ergonomics with Astro-grade performance, it would slot directly into the SHIFT64 worldview: own your stack, no reseller margins, no plugin treadmill, fast by construction. I was ready to recommend it.&lt;/p&gt;
&lt;p&gt;That’s the bias I walked into the benchmark with. Keep it in mind as you read the numbers.&lt;/p&gt;
&lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;/h2&gt;
&lt;p&gt;I wanted an apples-to-apples comparison, so I took deliberate care with the WordPress site. No Elementor, no page builder, no plugin soup. A &lt;strong&gt;hand-coded theme&lt;/strong&gt; (also scaffolded with Claude Code) modeled on the same content structure as the EmDash site, with &lt;strong&gt;only Object Cache&lt;/strong&gt; running. No full-page cache, no Cloudflare APO, no Varnish.&lt;/p&gt;
&lt;p&gt;Why no full-page cache on WordPress? Because EmDash’s Worker still does work on every request — it queries D1, it composes HTML. To compare a cached HTML file against a Worker doing real work would be dishonest. I wanted both backends doing similar amounts of work per request, then measure how long that work actually takes.&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;EmDash site&lt;/th&gt;&lt;th&gt;WordPress site&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Engine&lt;/td&gt;&lt;td&gt;Astro 6 + D1 (SQLite at edge)&lt;/td&gt;&lt;td&gt;PHP 8.4 + MySQL&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hosting&lt;/td&gt;&lt;td&gt;Cloudflare Workers (serverless)&lt;/td&gt;&lt;td&gt;Hetzner (Frankfurt)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CDN&lt;/td&gt;&lt;td&gt;Cloudflare (native)&lt;/td&gt;&lt;td&gt;Cloudflare (proxy)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;URL&lt;/td&gt;&lt;td&gt;emdashcms.pl&lt;/td&gt;&lt;td&gt;emdash.pl&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Caching&lt;/td&gt;&lt;td&gt;Official &lt;code&gt;emdash-blog&lt;/code&gt; template defaults — see below&lt;/td&gt;&lt;td&gt;Object Cache only&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Built with&lt;/td&gt;&lt;td&gt;Claude Code&lt;/td&gt;&lt;td&gt;Claude Code&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Network path&lt;/td&gt;&lt;td&gt;OVH Warsaw → CF Edge → D1&lt;/td&gt;&lt;td&gt;OVH Warsaw → CF Proxy → Hetzner Frankfurt (~400 km)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 id=&quot;what-emdash-defaults-actually-means&quot;&gt;What “EmDash defaults” actually means&lt;/h4&gt;
&lt;p&gt;The EmDash site runs the official &lt;a href=&quot;https://github.com/emdash-cms/emdash-blog&quot;&gt;&lt;code&gt;emdash-blog&lt;/code&gt; starter template&lt;/a&gt; unmodified — exactly what a developer gets the first time they scaffold an EmDash project. That template wires up three things adjacent to caching, and it is worth being precise about each, because “default caching” is doing a lot of heavy lifting in most online discussions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;D1 with &lt;code&gt;session: &amp;quot;auto&amp;quot;&lt;/code&gt;&lt;/strong&gt; — in &lt;code&gt;astro.config.mjs&lt;/code&gt;, the database binding is declared as &lt;code&gt;d1({ binding: &amp;quot;DB&amp;quot;, session: &amp;quot;auto&amp;quot; })&lt;/code&gt;. This enables D1’s &lt;a href=&quot;https://developers.cloudflare.com/d1/best-practices/read-replication/&quot;&gt;Sessions API&lt;/a&gt;, which is a &lt;em&gt;read-replication / read-after-write consistency&lt;/em&gt; hint for D1 itself. It is &lt;strong&gt;not&lt;/strong&gt; a page cache. Every query still goes over the wire.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Per-page &lt;code&gt;cacheHint&lt;/code&gt; emission&lt;/strong&gt; — every page that fetches content does it like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;entries&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;posts&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;cacheHint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getEmDashCollection&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;posts&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;Astro.cache.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(cacheHint);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both &lt;code&gt;getEmDashCollection()&lt;/code&gt; and &lt;code&gt;getEmDashEntry()&lt;/code&gt; return a &lt;code&gt;cacheHint&lt;/code&gt; describing how the result depends on the underlying collection / entry. The template dutifully feeds it into &lt;code&gt;Astro.cache.set()&lt;/code&gt; on &lt;code&gt;index.astro&lt;/code&gt;, &lt;code&gt;posts/index.astro&lt;/code&gt;, &lt;code&gt;posts/[slug].astro&lt;/code&gt; and &lt;code&gt;pages/[slug].astro&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A bare Worker handler&lt;/strong&gt; — &lt;code&gt;src/worker.ts&lt;/code&gt; is exactly three lines:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handler &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;@astrojs/cloudflare/entrypoints/server&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { PluginBridge } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;@emdash-cms/cloudflare/sandbox&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handler;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;caches.open()&lt;/code&gt;, no &lt;code&gt;caches.default&lt;/code&gt;, no Cache API wrapper, no middleware setting &lt;code&gt;Cache-Control&lt;/code&gt; on HTML responses. The only &lt;code&gt;Cache-Control&lt;/code&gt; header anywhere in the template lives on the RSS feed (&lt;code&gt;src/pages/rss.xml.ts&lt;/code&gt; → &lt;code&gt;public, max-age=3600&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now the catch. &lt;strong&gt;&lt;code&gt;Astro.cache.set()&lt;/code&gt; only does anything if there is an &lt;code&gt;experimental.cache&lt;/code&gt; provider configured&lt;/strong&gt; in &lt;code&gt;astro.config.mjs&lt;/code&gt; to actually consume the hints. The default &lt;code&gt;emdash-blog&lt;/code&gt; template &lt;strong&gt;does not configure one&lt;/strong&gt; — there is no &lt;code&gt;experimental: { cache: ... }&lt;/code&gt; block. So in the unmodified template, the &lt;code&gt;cacheHint&lt;/code&gt; mechanism is wired up at the call sites, but no consumer exists on the other end. The hints are emitted into a void.&lt;/p&gt;
&lt;p&gt;The practical consequence: &lt;strong&gt;on the default &lt;code&gt;emdash-blog&lt;/code&gt; template, every HTML request — human, curl, or Googlebot — runs the full data-fetching chain against D1 and renders the page fresh.&lt;/strong&gt; That is the baseline these numbers measure. No HTML caching is in play. The Sessions API is doing what it does for D1 reads, the cache hints are being emitted but not stored, and the Worker is just rendering the page on every hit.&lt;/p&gt;
&lt;p&gt;This is also exactly what we ran in the early days of the experiment, before any of the manual optimizations described later in this article. Everything starts from the unmodified starter.&lt;/p&gt;
&lt;p&gt;Note one more thing in the table: WordPress has a &lt;strong&gt;longer&lt;/strong&gt; network path. Every WP request hops through Cloudflare’s proxy and then crosses ~400 km to Frankfurt before any PHP runs. EmDash runs at the edge, in theory close to the test server. The geography favors EmDash. That’s important context for what comes next.&lt;/p&gt;
&lt;h3 id=&quot;methodology&quot;&gt;Methodology&lt;/h3&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Parameter&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Test period&lt;/td&gt;&lt;td&gt;2026-04-03 16:30 → 2026-04-07 06:28 UTC (~3.5 days)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Total measurements&lt;/td&gt;&lt;td&gt;&lt;strong&gt;4,732&lt;/strong&gt; (2,366 per site)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Runs&lt;/td&gt;&lt;td&gt;182&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pages tested&lt;/td&gt;&lt;td&gt;13 (homepage + 12 posts, identical content on both sites)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tool&lt;/td&gt;&lt;td&gt;&lt;code&gt;curl&lt;/code&gt; with full timing breakdown (&lt;code&gt;--write-out&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Test server&lt;/td&gt;&lt;td&gt;OVH VPS, Warsaw&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Isolation&lt;/td&gt;&lt;td&gt;New &lt;code&gt;curl&lt;/code&gt; process per URL — no keep-alive, no client cache&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Request order&lt;/td&gt;&lt;td&gt;Randomized per run (kills DNS-cache bias)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The cron schedule varied across three phases to force cold starts — 10-minute baseline (Workers stay warm) → 58-minute gap → 3-hour gap bursts firing &lt;code&gt;:00&lt;/code&gt;/&lt;code&gt;:01&lt;/code&gt;/&lt;code&gt;:02&lt;/code&gt; to separate cold-start cost from steady-state cleanly. &lt;strong&gt;Full phase-by-phase methodology, raw &lt;code&gt;results.csv&lt;/code&gt;, and &lt;code&gt;analyze.py&lt;/code&gt; in the &lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark/tree/main/benchmark&quot;&gt;benchmark repo&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-verdict-from-someone-who-reads-crawl-stats-for-a-living&quot;&gt;The verdict from someone who reads crawl stats for a living&lt;/h2&gt;
&lt;p&gt;Before I show you my own numbers, here’s the part that pushed me from “huh, that’s unexpected” to “OK, I have to write this up properly”.&lt;/p&gt;
&lt;p&gt;I sent the Search Console crawl-stats charts — just the download-time graphs, no labels, no commentary — to &lt;strong&gt;&lt;a href=&quot;https://www.linkedin.com/in/jacek-zmudzinski/&quot;&gt;Jacek Żmudziński&lt;/a&gt;&lt;/strong&gt;, Head of GEO &amp;amp; SEO at Makolab. At his level he isn’t personally picking through every client site any more — he runs a team that does — but he spent years living inside Google Search Console before that, and the instinct for what a healthy crawl-stats download-time chart looks like is the kind of thing you don’t unlearn. He grades these in his sleep.&lt;/p&gt;
&lt;p&gt;He came back with a four-tier read on what he saw, blind:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Download time band&lt;/th&gt;&lt;th&gt;Jacek’s verdict&lt;/th&gt;&lt;th&gt;Where my sites landed&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;~1,300 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;”Scrap and rebuild.”&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;EmDash post pages on cold starts&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Typical for crawl-optimized WP&lt;/td&gt;&lt;td&gt;”Normal, expected”&lt;/td&gt;&lt;td&gt;The hand-coded WP comparison site&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Well-tuned WordPress&lt;/td&gt;&lt;td&gt;”Decent”&lt;/td&gt;&lt;td&gt;What good WP hosting looks like&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Astro / static-class&lt;/td&gt;&lt;td&gt;&lt;strong&gt;”Exemplary.”&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;The class EmDash &lt;em&gt;should&lt;/em&gt; have been in&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two things hit me about this read.&lt;/p&gt;
&lt;p&gt;First, the exact tier where Jacek wanted a teardown — the ~1,300 ms zone — is exactly where my cold-start spikes were living. Independent confirmation, with zero knowledge of which technology produced which line.&lt;/p&gt;
&lt;p&gt;Second, the same expert had &lt;strong&gt;zero complaints&lt;/strong&gt; about the WordPress site. The hand-coded WP theme, with nothing more than Object Cache, landed in the “normal, expected, optimized for crawl” tier. Astro — which is what EmDash is built on top of — got the “exemplary” gold star.&lt;/p&gt;
&lt;p&gt;In other words: the technology choice EmDash is &lt;em&gt;built from&lt;/em&gt; gets the highest grade. The execution that EmDash &lt;em&gt;currently delivers&lt;/em&gt; gets “scrap and rebuild.” That gap is the whole story.&lt;/p&gt;
&lt;h2 id=&quot;what-search-console-actually-shows-after-one-week&quot;&gt;What Search Console actually shows after one week&lt;/h2&gt;
&lt;p&gt;Lab numbers are one thing. Field numbers are another. Both domains target the exact same keyword universe (&lt;code&gt;emdash cms&lt;/code&gt;, &lt;code&gt;em dash cms&lt;/code&gt;, &lt;code&gt;emdash cloudflare&lt;/code&gt;, etc.). One was built on the technology I was rooting for. One was the boring control. Both have been in Google Search Console for &lt;strong&gt;roughly one week&lt;/strong&gt; as of this writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;EmDash site (&lt;code&gt;emdashcms.pl&lt;/code&gt;)&lt;/strong&gt; — submitted to Search Console on &lt;strong&gt;2026-04-02&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WordPress site (&lt;code&gt;emdash.pl&lt;/code&gt;)&lt;/strong&gt; — submitted to Search Console on &lt;strong&gt;2026-04-03&lt;/strong&gt;, a day later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(The screenshots below show the “3 months” filter because that’s the GSC default view — but the actual data only exists from the submission dates onward. Everything before April is a flat line.)&lt;/p&gt;
&lt;p&gt;Before I show you the numbers, an important fairness note that makes the result &lt;em&gt;worse&lt;/em&gt;, not better, for EmDash:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The WordPress site was submitted a day later&lt;/strong&gt; — less time to accumulate impressions and clicks.&lt;/li&gt;
&lt;li&gt;Since publication, the WP site has received &lt;strong&gt;zero updates&lt;/strong&gt; — no new posts, no plugin changes, nothing. Set-and-forget, the way most SMB sites actually live in the wild.&lt;/li&gt;
&lt;li&gt;During the same window, the EmDash site received &lt;strong&gt;a cache strategy update and three new articles.&lt;/strong&gt; Active development.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So WordPress had a &lt;em&gt;shorter&lt;/em&gt; indexing window, less content, and zero ongoing investment. EmDash had more time, more content, and active engineering. Here is what one week of indexing produced.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WordPress site (&lt;code&gt;emdash.pl&lt;/code&gt;) — 1 week in Search Console (submitted 2026-04-03):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/emdash/wp.jpeg&quot; alt=&quot;Search Console Performance for the WordPress comparison site, showing 65 clicks, 195 impressions, 33.3% CTR and average position 3.5&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EmDash site (&lt;code&gt;emdashcms.pl&lt;/code&gt;) — 1 week in Search Console (submitted 2026-04-02):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/emdash/em.jpeg&quot; alt=&quot;Search Console Performance for the EmDash showcase site, showing 15 clicks, 175 impressions, 8.6% CTR and average position 7.1&quot;/&gt;&lt;/p&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Search Console (1 week since submission)&lt;/th&gt;&lt;th&gt;WordPress (&lt;code&gt;emdash.pl&lt;/code&gt;)&lt;/th&gt;&lt;th&gt;EmDash (&lt;code&gt;emdashcms.pl&lt;/code&gt;)&lt;/th&gt;&lt;th&gt;Winner&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total clicks&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;65&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;WP — 4.3× more&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Total impressions&lt;/td&gt;&lt;td&gt;195&lt;/td&gt;&lt;td&gt;175&lt;/td&gt;&lt;td&gt;~tied&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CTR&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;33.3%&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;8.6%&lt;/td&gt;&lt;td&gt;WP — 3.9× higher&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Average position&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3.5&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;7.1&lt;/td&gt;&lt;td&gt;WP — half the rank&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Top brand query — &lt;code&gt;emdash cms&lt;/code&gt; (clicks)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;48&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Top brand query — &lt;code&gt;emdash cms&lt;/code&gt; (position)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3.4&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;13.7&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Top brand query — &lt;code&gt;em dash cms&lt;/code&gt; (position)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;2.3&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;4.3&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read those last three rows again.&lt;/p&gt;
&lt;p&gt;The WordPress site — the one I built in a day, never touched again, started a day late — outranks the EmDash showcase site &lt;strong&gt;on the EmDash brand query itself.&lt;/strong&gt; Position 3.4 vs 13.7. Forty-eight clicks vs one. On a query where the EmDash site is, by name, the better topical match.&lt;/p&gt;
&lt;p&gt;I want to be careful here. SEO is multi-causal. &lt;strong&gt;One week is very short&lt;/strong&gt; — these numbers will move. Brand attribution is messy. The impression counts are within noise of each other, which suggests Google is showing both sites for similar queries. But two facts are hard to argue with even at this sample size:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Average position differs by half a rank tier&lt;/strong&gt; (3.5 vs 7.1) — and Google’s ranking signals are dominated by Core Web Vitals, page experience, and crawl health, all of which favor the WP site based on the lab data above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CTR differs by ~4×&lt;/strong&gt; (33.3% vs 8.6%) on similar impression volume — which is what you’d expect when one site loads in 100 ms and the other has a 35% chance of a &amp;gt;800 ms cold start during business hours.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I cannot prove from one screenshot pair that performance caused the SEO gap. I &lt;em&gt;can&lt;/em&gt; say that the site Jacek filed in the &lt;strong&gt;“scrap and rebuild”&lt;/strong&gt; tier is losing to the site he filed as &lt;strong&gt;“normal, expected”&lt;/strong&gt; on every Search Console metric that counts — and that the tier EmDash was &lt;em&gt;supposed&lt;/em&gt; to be in, “exemplary” Astro-class, was a tier it never actually reached. That is not the direction the chart was supposed to go.&lt;/p&gt;
&lt;h2 id=&quot;the-numbers--server-processing-time&quot;&gt;The numbers — server processing time&lt;/h2&gt;
&lt;p&gt;Server processing time is TTFB minus DNS, TCP and SSL. It’s pure backend work: database queries, HTML rendering. It isolates the part you can actually fix.&lt;/p&gt;




































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;EmDash (CF Workers + D1)&lt;/th&gt;&lt;th&gt;WordPress (Hetzner)&lt;/th&gt;&lt;th&gt;Delta&lt;/th&gt;&lt;th&gt;Winner&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;543 ms&lt;/td&gt;&lt;td&gt;84 ms&lt;/td&gt;&lt;td&gt;+459 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;WP — 6.4× faster&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Median&lt;/td&gt;&lt;td&gt;543 ms&lt;/td&gt;&lt;td&gt;69 ms&lt;/td&gt;&lt;td&gt;+474 ms&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Min&lt;/td&gt;&lt;td&gt;129 ms&lt;/td&gt;&lt;td&gt;42 ms&lt;/td&gt;&lt;td&gt;+87 ms&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Max&lt;/td&gt;&lt;td&gt;2,196 ms&lt;/td&gt;&lt;td&gt;246 ms&lt;/td&gt;&lt;td&gt;+1,950 ms&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P95&lt;/td&gt;&lt;td&gt;864 ms&lt;/td&gt;&lt;td&gt;148 ms&lt;/td&gt;&lt;td&gt;+716 ms&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P99&lt;/td&gt;&lt;td&gt;1,121 ms&lt;/td&gt;&lt;td&gt;234 ms&lt;/td&gt;&lt;td&gt;+887 ms&lt;/td&gt;&lt;td&gt;WP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Std Dev&lt;/td&gt;&lt;td&gt;178 ms&lt;/td&gt;&lt;td&gt;42 ms&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CV&lt;/td&gt;&lt;td&gt;32.8%&lt;/td&gt;&lt;td&gt;49.7%&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;EmDash (more consistent)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;WordPress’s &lt;strong&gt;worst&lt;/strong&gt; measurement (246 ms) is faster than EmDash’s &lt;strong&gt;best&lt;/strong&gt; post-page measurement (~400 ms). That’s not a margin you optimize away. That’s a different category of latency.&lt;/p&gt;
&lt;p&gt;The CV column is the only place EmDash technically wins. Cloudflare Workers are &lt;em&gt;consistently&lt;/em&gt; slow rather than &lt;em&gt;occasionally&lt;/em&gt; fast, which matters for capacity planning but does not help your user.&lt;/p&gt;
&lt;h3 id=&quot;total-ttfb--what-the-user-actually-waits-for&quot;&gt;Total TTFB — what the user actually waits for&lt;/h3&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;EmDash&lt;/th&gt;&lt;th&gt;WordPress&lt;/th&gt;&lt;th&gt;Delta&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Mean&lt;/td&gt;&lt;td&gt;596 ms&lt;/td&gt;&lt;td&gt;136 ms&lt;/td&gt;&lt;td&gt;+460 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Median&lt;/td&gt;&lt;td&gt;593 ms&lt;/td&gt;&lt;td&gt;116 ms&lt;/td&gt;&lt;td&gt;+477 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P95&lt;/td&gt;&lt;td&gt;939 ms&lt;/td&gt;&lt;td&gt;233 ms&lt;/td&gt;&lt;td&gt;+706 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Max&lt;/td&gt;&lt;td&gt;2,294 ms&lt;/td&gt;&lt;td&gt;373 ms&lt;/td&gt;&lt;td&gt;+1,921 ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;network-breakdown&quot;&gt;Network breakdown&lt;/h3&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Phase&lt;/th&gt;&lt;th&gt;EmDash&lt;/th&gt;&lt;th&gt;WordPress&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DNS&lt;/td&gt;&lt;td&gt;3 ms&lt;/td&gt;&lt;td&gt;3 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TCP&lt;/td&gt;&lt;td&gt;1 ms&lt;/td&gt;&lt;td&gt;1 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SSL&lt;/td&gt;&lt;td&gt;49 ms&lt;/td&gt;&lt;td&gt;47 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;543 ms&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;84 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Network is identical. The 459 ms gap is &lt;strong&gt;entirely backend work&lt;/strong&gt;. Every chart in this article is really one chart with different y-axes: it’s the cost of fetching a content page from D1 at the edge versus from a local MySQL on the same physical box as PHP.&lt;/p&gt;
&lt;h3 id=&quot;per-page-comparison--wp-wins-13--13&quot;&gt;Per-page comparison — WP wins 13 / 13&lt;/h3&gt;
&lt;p&gt;Per-page numbers are remarkably consistent. WordPress wins every single one of the 13 tested pages. The EmDash homepage (225 ms) is the only sub-300 ms result EmDash delivers — and it cheats, because the homepage runs a single post-list query. Every EmDash post page clusters at 550–590 ms, because it chains three or more sequential D1 round-trips (entry → tags + related → related tags) at ~50–100 ms each. WordPress covers all 13 pages in 78–91 ms. &lt;strong&gt;Full per-page table in the &lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark/blob/main/benchmark/report-v2.md&quot;&gt;benchmark report&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is not a code-quality issue. EmDash uses &lt;code&gt;Promise.all()&lt;/code&gt; where it can. The dependency chain is what it is. &lt;strong&gt;The bottleneck is the architecture, not the implementation.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;cold-starts-are-real-and-they-follow-the-working-day&quot;&gt;Cold starts are real and they follow the working day&lt;/h2&gt;
&lt;p&gt;Once I forced 3-hour gaps to make Workers actually go to sleep, cold starts became the dominant story.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Mean&lt;/th&gt;&lt;th&gt;Median&lt;/th&gt;&lt;th&gt;P95&lt;/th&gt;&lt;th&gt;Max&lt;/th&gt;&lt;th&gt;n&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Cold&lt;/strong&gt; (&lt;code&gt;:00&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;649 ms&lt;/td&gt;&lt;td&gt;548 ms&lt;/td&gt;&lt;td&gt;1,121 ms&lt;/td&gt;&lt;td&gt;2,196 ms&lt;/td&gt;&lt;td&gt;260&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Warm&lt;/strong&gt; (&lt;code&gt;:01&lt;/code&gt;/&lt;code&gt;:02&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;549 ms&lt;/td&gt;&lt;td&gt;519 ms&lt;/td&gt;&lt;td&gt;703 ms&lt;/td&gt;&lt;td&gt;939 ms&lt;/td&gt;&lt;td&gt;520&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The headline number (+18% on average) hides the more important fact: &lt;strong&gt;the &amp;gt;800 ms zone is cold-start exclusive.&lt;/strong&gt; Out of 520 warm requests, exactly 2 crossed 800 ms. Out of 260 cold requests, 50 did. That’s a 48× higher chance of a slow page when the Worker had to spin up.&lt;/p&gt;
&lt;p&gt;Cold starts also follow a daily cycle that maps almost exactly to the working day: mildest at 01:00–04:00 UTC (~550 ms average, 8–12% spike rate), worst at 10:00 UTC (&lt;strong&gt;745 ms average, 35% of requests crossing 800 ms, max 2.2 s&lt;/strong&gt;). This correlates with global Cloudflare platform load, which I cannot control. &lt;strong&gt;Full hourly breakdown in the &lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark/blob/main/benchmark/report-v2.md&quot;&gt;benchmark report&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WordPress, for the record, shows &lt;strong&gt;zero&lt;/strong&gt; cold-start effect:&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Mean&lt;/th&gt;&lt;th&gt;Max&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;WP cold (&lt;code&gt;:00&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;80 ms&lt;/td&gt;&lt;td&gt;243 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;WP warm (&lt;code&gt;:01&lt;/code&gt;/&lt;code&gt;:02&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;84 ms&lt;/td&gt;&lt;td&gt;239 ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Negative delta on cold. There is no cold start. PHP-FPM pools are warm because the box never sleeps.&lt;/p&gt;
&lt;h2 id=&quot;i-paid-cloudflare-5-to-fix-this-it-got-slightly-worse&quot;&gt;I paid Cloudflare $5 to fix this. It got slightly worse.&lt;/h2&gt;
&lt;p&gt;Halfway through the experiment, I upgraded the EmDash account from the Free plan to the &lt;strong&gt;Workers Paid plan&lt;/strong&gt; ($5/mo). Cloudflare’s docs don’t promise improved TTFB on the paid plan, but for $5 I wanted to be thorough.&lt;/p&gt;
&lt;p&gt;The paid plan gives you more CPU time per request, more total requests per month, and Durable Objects. It does not, as far as I can tell, address D1 latency. The numbers confirm it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mean server processing: +72 ms (+14%) on paid vs free&lt;/strong&gt; — 522 ms → 594 ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;% of requests over 800 ms: jumped from 5.0% to 8.5%&lt;/strong&gt; on paid&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;% of requests over 1,000 ms: jumped from 1.8% to 4.3%&lt;/strong&gt; on paid&lt;/li&gt;
&lt;li&gt;WordPress control group moved by 1 ms in the same window — confirms the difference on the EmDash site is real, not test-server noise&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After controlling for time of day (matching cold-start hours), paid is still ~52 ms slower on average. The result varies by hour and could be Worker pool placement noise, so I am not going to claim paid is categorically &lt;em&gt;worse&lt;/em&gt;. But if you are upgrading because you read somewhere that paid Workers are faster: &lt;strong&gt;save your $5.&lt;/strong&gt; The bottleneck is downstream of anything CPU limits will fix. Full Free-vs-Paid breakdown in the &lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark/blob/main/benchmark/report-v2.md&quot;&gt;benchmark report&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-i-tried-to-do-about-it&quot;&gt;What I tried to do about it&lt;/h2&gt;
&lt;p&gt;After the initial benchmark, I went into the EmDash code and did everything I could think of to reduce D1 round-trips. Two changes mattered.&lt;/p&gt;
&lt;h3 id=&quot;1-serverdefer-on-widget-areas&quot;&gt;1. &lt;code&gt;server:defer&lt;/code&gt; on widget areas&lt;/h3&gt;
&lt;p&gt;Every post page rendered a sidebar (search, categories, tags, recent posts, archives) and a footer widget area before responding. Each widget made its own D1 query. The sidebar alone fired &lt;strong&gt;7 queries&lt;/strong&gt; on the critical path.&lt;/p&gt;
&lt;p&gt;Astro’s &lt;code&gt;server:defer&lt;/code&gt; directive moves a component out of the initial response — the page ships immediately with a placeholder, and the deferred component loads via a follow-up micro-request. The work still happens, but it doesn’t block TTFB.&lt;/p&gt;
&lt;p&gt;This is not free. &lt;code&gt;server:defer&lt;/code&gt; does not apply to components imported from &lt;code&gt;node_modules&lt;/code&gt;, so it requires writing thin wrapper components inside the project just to gain access to the directive, plus skeleton fallbacks so the page does not pop when the deferred content arrives. None of it is documented as a thing you have to do. &lt;strong&gt;It is not part of the out-of-the-box EmDash experience.&lt;/strong&gt; Full wrapper pattern + fallbacks are in the &lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark&quot;&gt;EmDash repo&lt;/a&gt;, on the &lt;code&gt;perf/server-defer-widgets&lt;/code&gt; branch.&lt;/p&gt;
&lt;h3 id=&quot;2-killing-an-n1-with-an-undocumented-batch-api&quot;&gt;2. Killing an N+1 with an undocumented batch API&lt;/h3&gt;
&lt;p&gt;Every page that lists posts (homepage, archive, post detail with “read more”) was fetching tags per post in an N+1 loop — one D1 query per post. EmDash exposes &lt;code&gt;getTermsForEntries&lt;/code&gt;, a batch API that fetches all tags in a single &lt;code&gt;WHERE entry_id IN (...)&lt;/code&gt; query. It is &lt;strong&gt;undocumented&lt;/strong&gt;. I found it by reading source. Applied across 5 listing pages (&lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/posts&lt;/code&gt;, &lt;code&gt;/posts/[slug]&lt;/code&gt;, &lt;code&gt;/category/[slug]&lt;/code&gt;, &lt;code&gt;/tag/[slug]&lt;/code&gt;). Full before/after diff in the &lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark&quot;&gt;EmDash repo&lt;/a&gt; on the &lt;code&gt;perf/server-defer-widgets&lt;/code&gt; branch.&lt;/p&gt;
&lt;h3 id=&quot;what-it-bought-me&quot;&gt;What it bought me&lt;/h3&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Before&lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;th&gt;Delta&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Server mean&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;542 ms&lt;/td&gt;&lt;td&gt;322 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;−220 ms (−41%)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Server median&lt;/td&gt;&lt;td&gt;547 ms&lt;/td&gt;&lt;td&gt;317 ms&lt;/td&gt;&lt;td&gt;−230 ms (−42%)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TTFB mean&lt;/td&gt;&lt;td&gt;593 ms&lt;/td&gt;&lt;td&gt;373 ms&lt;/td&gt;&lt;td&gt;−220 ms (−37%)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P95&lt;/td&gt;&lt;td&gt;732 ms&lt;/td&gt;&lt;td&gt;368 ms&lt;/td&gt;&lt;td&gt;−364 ms (−50%)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P99&lt;/td&gt;&lt;td&gt;1,094 ms&lt;/td&gt;&lt;td&gt;424 ms&lt;/td&gt;&lt;td&gt;−670 ms (−61%)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Max spike&lt;/td&gt;&lt;td&gt;2,196 ms&lt;/td&gt;&lt;td&gt;745 ms&lt;/td&gt;&lt;td&gt;−1,451 ms (−66%)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CV (consistency)&lt;/td&gt;&lt;td&gt;29.3%&lt;/td&gt;&lt;td&gt;11.6%&lt;/td&gt;&lt;td&gt;2.5× more stable&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;41% off the mean, 61% off the P99, max spike cut by two-thirds. That’s a real win — and I’d take it on any project. &lt;strong&gt;It is not enough.&lt;/strong&gt;&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;EmDash (optimized)&lt;/th&gt;&lt;th&gt;WordPress&lt;/th&gt;&lt;th&gt;Ratio&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Server mean&lt;/td&gt;&lt;td&gt;322 ms&lt;/td&gt;&lt;td&gt;78 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;4.1×&lt;/strong&gt; (was 6.4×)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Server median&lt;/td&gt;&lt;td&gt;317 ms&lt;/td&gt;&lt;td&gt;53 ms&lt;/td&gt;&lt;td&gt;6.0×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;P95&lt;/td&gt;&lt;td&gt;368 ms&lt;/td&gt;&lt;td&gt;142 ms&lt;/td&gt;&lt;td&gt;2.6×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Max spike&lt;/td&gt;&lt;td&gt;745 ms&lt;/td&gt;&lt;td&gt;245 ms&lt;/td&gt;&lt;td&gt;3.0×&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;After every optimization I could think of, &lt;strong&gt;WordPress is still 4× faster&lt;/strong&gt;. The remaining ~320 ms is the floor — roughly 8 sequential D1 round-trips at ~40 ms each. There is no code-level fix for that. It is the cost of asking a database that lives at the edge to answer a question that requires three follow-up questions.&lt;/p&gt;
&lt;h2 id=&quot;and-then-i-added-edge-caching&quot;&gt;And then I added edge caching&lt;/h2&gt;
&lt;p&gt;Once I’d exhausted the data-fetching optimizations, the only remaining lever was full-page caching. I added a &lt;code&gt;Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=300&lt;/code&gt; header via middleware — 60 s fresh at the edge, then 5 minutes of stale-while-revalidate.&lt;/p&gt;
&lt;p&gt;There’s a trap here that ate an hour of my life: &lt;strong&gt;Workers on a custom domain bypass Cloudflare’s CDN cache layer by default.&lt;/strong&gt; The standard &lt;code&gt;Cache-Control&lt;/code&gt; header has no effect — responses go straight from Worker to client. To actually cache, the Worker entrypoint has to be wrapped in explicit &lt;code&gt;caches.open()&lt;/code&gt; / &lt;code&gt;match()&lt;/code&gt; / &lt;code&gt;put()&lt;/code&gt; calls. Full middleware + wrapper implementation in the &lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark&quot;&gt;EmDash repo&lt;/a&gt;. Once that was in:&lt;/p&gt;























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Page&lt;/th&gt;&lt;th&gt;Uncached (optimized)&lt;/th&gt;&lt;th&gt;Cached HIT&lt;/th&gt;&lt;th&gt;Improvement&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Homepage&lt;/td&gt;&lt;td&gt;294 ms&lt;/td&gt;&lt;td&gt;~150 ms&lt;/td&gt;&lt;td&gt;−49%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Post pages&lt;/td&gt;&lt;td&gt;~325 ms&lt;/td&gt;&lt;td&gt;~110 ms&lt;/td&gt;&lt;td&gt;−66%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Cached EmDash post pages are now &lt;strong&gt;faster&lt;/strong&gt; than uncached WordPress.&lt;/p&gt;
&lt;p&gt;I want to be brutally clear about what this proves: &lt;strong&gt;nothing.&lt;/strong&gt; WordPress without caching, vs EmDash with caching, is not a fair comparison. WordPress with WP Super Cache, or Cloudflare APO, or even a basic Varnish, would land in the same sub-100 ms territory. The point of the caching round was not to declare a winner. It was to confirm that &lt;strong&gt;edge caching is a workable mitigation&lt;/strong&gt; for D1 latency, in case you want to ship EmDash to production with eyes open.&lt;/p&gt;
&lt;p&gt;It is also exactly the lever that does not help you with the audience that started this whole article.&lt;/p&gt;
&lt;h2 id=&quot;why-caching-does-not-save-you-on-search-console&quot;&gt;Why caching does not save you on Search Console&lt;/h2&gt;
&lt;p&gt;Here’s the part that sealed the verdict for me.&lt;/p&gt;
&lt;p&gt;The two domains are still live as of this writing. Since the benchmark ended:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;WordPress site&lt;/strong&gt; received zero updates. Zero plugin changes, zero new posts, zero touches. Set-and-forget, the way most SMB sites actually live in the wild.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;EmDash site&lt;/strong&gt; received an updated cache strategy and &lt;strong&gt;three new articles&lt;/strong&gt;. Active development.&lt;/li&gt;
&lt;li&gt;I &lt;strong&gt;stopped warming&lt;/strong&gt; the EmDash Worker (no more cron pinging it every 10 minutes). It is now serving traffic the way a real low-traffic SMB site would.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What happened to crawl stats?&lt;/p&gt;
&lt;p&gt;The cache strategy update &lt;strong&gt;had no measurable effect on crawl response time&lt;/strong&gt;. And once the warming stopped, &lt;strong&gt;cold starts actually got worse.&lt;/strong&gt; Googlebot is the perfect cold-start victim: it shows up irregularly, often hits URLs nothing else has visited recently, and gets exactly zero benefit from the Worker being warm for human users five minutes ago. Every Googlebot fetch is, statistically, a cold fetch.&lt;/p&gt;
&lt;p&gt;Edge caching helps the human user who loads a page that someone else loaded 30 seconds earlier. It does very little for a crawler that walks your sitemap once a day and sees a different URL each time. That’s why Jacek’s blind read landed where it did.&lt;/p&gt;
&lt;p&gt;This is also why “just turn on cache” is not the answer for the use case I was originally excited about. If your client’s site needs to rank, the bot’s experience matters. And the bot keeps walking into a cold Worker.&lt;/p&gt;
&lt;h2 id=&quot;after-jacek-read-the-full-article&quot;&gt;After Jacek read the full article&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.linkedin.com/in/jacek-zmudzinski/&quot;&gt;Jacek Żmudziński&lt;/a&gt; — Head of GEO &amp;amp; SEO at Makolab, who already gave the blind verdict on the crawl-stats charts above — read the full preview and wrote back. With his permission, I am reproducing his note here in full, because it puts the SEO/GEO angle on this whole experiment better than I did:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I very often come across WordPress websites that are slow, overloaded with plugins, and create serious Cost of Retrieval issues. This is a key topic in SEO today — search engines pay the real cost of crawling, rendering, and indexing content. In the context of GEO and building visibility in AI, this efficiency matters even more.&lt;/p&gt;
&lt;p&gt;WordPress has earned a reputation for being ‘heavy,’ but this benchmark is a real eye-opener. Seeing a hand coded WP theme outperform a modern edge/serverless stack by 6.4x proves that the engine isn’t the problem. It’s the technical debt and poor implementation.&lt;/p&gt;
&lt;p&gt;That’s the real WordPress paradox. Plugins are both its biggest advantage and its biggest risk. WordPress itself isn’t the issue; it’s how it’s maintained. A well-built WP site is still a performance benchmark that’s very hard to beat.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The phrase &lt;strong&gt;Cost of Retrieval&lt;/strong&gt; in Jacek’s first paragraph is doing a lot of work and is worth pausing on. It is the unspoken bill on every page of every site you ship — every byte a crawler downloads, every millisecond it waits, every render it has to finish to extract something usable. Crawlers operate on a budget, and they are the ones who decide which pages on your site get re-fetched often enough to stay fresh in the index. A site that costs Googlebot 400 ms per fetch instead of 80 ms is not just “five times slower” in some abstract sense; it is a site that gets re-crawled less often, whose updates land in the index later, and whose answers stay stale longer in the AI summaries that quietly read from the same crawl. That is the real downstream cost the EmDash side of this benchmark is paying — and it is the part Jacek puts a name on that the rest of this article had only been gesturing at.&lt;/p&gt;
&lt;h2 id=&quot;wordpress-is-absurdly-fast--when-you-dont-bury-it-in-plugins&quot;&gt;WordPress is absurdly fast — when you don’t bury it in plugins&lt;/h2&gt;
&lt;p&gt;I want to take one quick detour from beating up EmDash to do something I almost never see done in 2026: &lt;strong&gt;say something nice about WordPress.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here is what the inside of the WordPress comparison site actually looks like, measured by &lt;a href=&quot;https://wordpress.org/plugins/query-monitor/&quot;&gt;Query Monitor&lt;/a&gt; on production. Same hand-coded theme that produced the 84 ms server-processing mean in the benchmark above. No page builder. No plugin soup. Object Cache + Zend OPcache. That’s it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Homepage (&lt;code&gt;emdash.pl/&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/emdash/wp-querymonitor-home.jpeg&quot; alt=&quot;Query Monitor on emdash.pl homepage — 0.0154s page generation, 2 database queries in 0.0003s, 4.3 MB peak memory, Object Cache 93.3% hit rate, Zend OPcache active&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post page (&lt;code&gt;emdash.pl/przyszlosc-cms-emdash/&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/emdash/wp-querymonitor-post.jpeg&quot; alt=&quot;Query Monitor on emdash.pl post page — 0.0379s page generation, 3 database queries in 0.0005s, 4.3 MB peak memory, Object Cache 95.1% hit rate, Zend OPcache active&quot;/&gt;&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Homepage&lt;/th&gt;&lt;th&gt;Post page&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Page Generation Time&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;15.4 ms&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;37.9 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Database Queries&lt;/td&gt;&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Total query time&lt;/td&gt;&lt;td&gt;0.3 ms&lt;/td&gt;&lt;td&gt;0.5 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Peak Memory&lt;/td&gt;&lt;td&gt;4.3 MB (1.7% of 256 MB limit)&lt;/td&gt;&lt;td&gt;4.3 MB (1.7% of 256 MB limit)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;HTTP API Calls&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Object Cache hit rate&lt;/td&gt;&lt;td&gt;93.3% (1,228 hits, 88 misses)&lt;/td&gt;&lt;td&gt;95.1% (1,172 hits, 61 misses)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Opcode Cache&lt;/td&gt;&lt;td&gt;Zend OPcache&lt;/td&gt;&lt;td&gt;Zend OPcache&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Read those numbers slowly.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PHP is generating the entire page in &lt;strong&gt;15 milliseconds&lt;/strong&gt; on the homepage and &lt;strong&gt;38 milliseconds&lt;/strong&gt; on a post page. The database is being asked 2 or 3 questions, and answering them in &lt;em&gt;under half a millisecond combined&lt;/em&gt;. Object Cache is hitting 93–95% of the time, which is why MySQL is barely involved at all. Memory footprint is 4.3 MB on a 256 MB limit. There is no HTTP API call. There is no remote anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the unfashionable truth of WordPress in 2026:&lt;/strong&gt; the engine itself is &lt;em&gt;very&lt;/em&gt; fast. It is fast in the way a well-tuned C program is fast. PHP 8.4 with OPcache, a sane object cache, a hand-written theme, and a database with no plugin gunk in it — that stack will outrun a lot of “modern” alternatives, and the screenshots above are just the receipts.&lt;/p&gt;
&lt;p&gt;The reason WordPress has a reputation for being slow is &lt;strong&gt;earned&lt;/strong&gt; — but earned by the &lt;em&gt;wrong&lt;/em&gt; WordPress. By Elementor. By Divi. By bad ThemeForest themes. By “we just installed a caching plugin.” By 47 active plugins each registering its own admin-ajax handler on every front-end page load. By a theme bloater plus three “SEO” plugins plus a contact form plugin plus a Stripe plugin plus a “we needed analytics” plugin. &lt;em&gt;That&lt;/em&gt; WordPress will absolutely hand you a 2–3 second TTFB on a shared host. And then someone migrates that monstrosity to a serverless edge stack and reports “WordPress was slow, the new stack is fast.”&lt;/p&gt;
&lt;p&gt;Yes — but you didn’t fix WordPress. You moved the bloat off WordPress.&lt;/p&gt;
&lt;p&gt;If you take only one thing from this whole article, it should not be “EmDash is bad.” It should be: &lt;strong&gt;before you rewrite your stack to escape a slow WordPress site, ask whether you actually have a slow WordPress site, or whether you have a fast WordPress engine being smothered by 47 plugins you can simply uninstall.&lt;/strong&gt; A hand-coded theme on the same hardware will frequently be the cheaper, faster, less risky migration than any rewrite — including this one.&lt;/p&gt;
&lt;p&gt;The hand-coded WordPress here is the &lt;em&gt;boring&lt;/em&gt; answer. It is not exciting. It is not on the front page of Hacker News. It is what actually runs almost every business website that has stayed up and ranked for a decade. The reason this benchmark exists at all is that I assumed something newer would do the job better. The data did not cooperate.&lt;/p&gt;
&lt;h2 id=&quot;where-emdash-might-actually-win--and-why-my-use-case-wasnt-it&quot;&gt;Where EmDash might actually win — and why my use case wasn’t it&lt;/h2&gt;
&lt;p&gt;I want to be careful here, because the wrong takeaway from this article is “EmDash is bad”. It isn’t. &lt;strong&gt;It’s wrong for the use case I evaluated it on&lt;/strong&gt; — and that use case has a name: low-traffic content sites for small businesses, the kind where Googlebot is half your daily traffic and the marketing director updates the team page once a quarter.&lt;/p&gt;
&lt;p&gt;If you flip the variables, the picture changes a lot.&lt;/p&gt;
&lt;p&gt;Imagine a site with &lt;strong&gt;millions of pageviews a month&lt;/strong&gt;. A news outlet, a high-volume e-commerce content hub, a global SaaS docs site. Everything I measured as a &lt;em&gt;problem&lt;/em&gt; in this article inverts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Worker is &lt;strong&gt;never cold.&lt;/strong&gt; It serves a steady stream of requests around the clock. The 18% cold-start penalty becomes a rounding error, because almost nothing is ever cold.&lt;/li&gt;
&lt;li&gt;D1 reads are &lt;strong&gt;hot in local edge caches.&lt;/strong&gt; The same handful of queries hit the same shards constantly, and the platform’s internal caching layers have actual data to work with.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;edge cache hit rate is high.&lt;/strong&gt; With enough traffic to keep popular pages warm, &lt;code&gt;s-maxage=60&lt;/code&gt; produces a near-100% hit rate instead of the near-zero rate Googlebot sees on a quiet site.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;operational story is genuinely good.&lt;/strong&gt; No servers to patch, no PHP version to upgrade, no “your site is down because the host migrated to a new IP.” For a 50M-pageview operation, deleting an entire layer of infrastructure is worth real money — possibly more than the latency gap costs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I haven’t measured this scenario. I would not be shocked if a high-traffic EmDash deployment looks competitive with — or even beats — a comparably-managed WordPress one, exactly because the cold-start problem disappears at scale and the “no infrastructure” pitch starts paying for itself in real ops budget. That’s a different article and someone else’s measurements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But that wasn’t my use case.&lt;/strong&gt; My pitch for EmDash was the opposite end of the traffic curve: small-business sites, hundreds to a few thousand visits per month, content updated occasionally by a non-technical owner. At that traffic level, every dynamic the high-traffic story relies on inverts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Worker &lt;strong&gt;is cold most of the time&lt;/strong&gt;, because traffic is sporadic and irregular.&lt;/li&gt;
&lt;li&gt;Googlebot is a disproportionately large slice of total hits — and Googlebot fetches are always, statistically, cold.&lt;/li&gt;
&lt;li&gt;The edge cache rarely warms up, because there isn’t enough repeat traffic to keep popular pages inside the &lt;code&gt;s-maxage&lt;/code&gt; window.&lt;/li&gt;
&lt;li&gt;You’re fighting cold starts continuously, on exactly the pages that matter most for SEO.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For &lt;em&gt;that&lt;/em&gt; segment — the segment EmDash was supposed to be the small-business answer to — the data in this article is unambiguous: &lt;strong&gt;it does not solve the problem I hoped it would solve, and it is measurably slower than the WordPress comparison site I built specifically as a control.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you’re running a high-traffic content operation and you find yourself in a “no more servers, please” mood, go test EmDash for real — your numbers might tell a very different story than mine, and the operational savings might be the deciding variable rather than the latency gap. But if your job is giving a small-business client a fast, low-maintenance content site that has to rank, this experiment is not encouraging.&lt;/p&gt;
&lt;h2 id=&quot;what-this-means-for-shift64&quot;&gt;What this means for SHIFT64&lt;/h2&gt;
&lt;p&gt;I started this experiment hoping to add a third tool to the SHIFT64 stack. Right now, the &lt;a href=&quot;https://shift64.com/blog/shift64-built-with-astro&quot;&gt;strategy doc&lt;/a&gt; is honest about what we ship: &lt;strong&gt;Astro for static, WordPress for content-heavy, WooCommerce for commerce.&lt;/strong&gt; EmDash was supposed to fold into that as “the editable Astro for SMBs”. After 4,732 measurements, I’m not ready to recommend it for that role yet.&lt;/p&gt;
&lt;p&gt;Here’s the lens I’m now using. For each of the three use cases I care about:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Use case&lt;/th&gt;&lt;th&gt;What I’d ship today&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Static / marketing site&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Astro (the way &lt;a href=&quot;https://shift64.com/blog/shift64-built-with-astro&quot;&gt;shift64.com itself is built&lt;/a&gt;)&lt;/td&gt;&lt;td&gt;Sub-100 ms TTFB without thinking about it. Zero ops.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Content site that an owner edits&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;WordPress, hand-coded theme, &lt;a href=&quot;https://shift64.com/blog/why-hosting-should-be-step-one-in-wordpress-optimization&quot;&gt;proper hosting&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Boring, fast, and the editing experience already exists.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;WooCommerce&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;WooCommerce on dedicated, high-frequency hardware&lt;/td&gt;&lt;td&gt;This is what SHIFT64 actually does for a living.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Where I’d revisit EmDash:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare ships D1 read replicas&lt;/strong&gt; that cut per-query latency by 5–10×. This is the single change that would move EmDash from “4× slower than WP” into “competitive with WP”. It is not currently available.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Astro’s &lt;code&gt;experimental.cache&lt;/code&gt; provider lands for the Cloudflare adapter&lt;/strong&gt;, which would let EmDash do real tag-based purge-on-edit instead of the 60-second &lt;code&gt;s-maxage&lt;/code&gt; window. That removes the staleness trade-off without losing the cache benefit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An EmDash release where &lt;code&gt;server:defer&lt;/code&gt; and batch APIs are the default&lt;/strong&gt;, not the careful manual optimization I had to discover.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If two of those three land, this becomes a different article.&lt;/p&gt;
&lt;h2 id=&quot;caveats-i-want-to-head-off&quot;&gt;Caveats I want to head off&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“You ran an old version.”&lt;/strong&gt; Maybe. But I worked from the official starter and the documented APIs at the time of testing. If there is a better stack inside EmDash that I missed, it is not in the docs, not in the starter, not in the published examples, and not findable in the time a real consultant would spend evaluating a CMS. That itself is a finding.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“You should have used [X].”&lt;/strong&gt; If “X” is a configuration that is publicly documented as the recommended setup, please point me to it on X or LinkedIn — happy to re-run.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“You should have tested a ‘realistic’ WordPress site — Elementor, Bricks, page builder, plugin soup.”&lt;/strong&gt; No, and here’s exactly why. EmDash has no visual page builder. You write &lt;code&gt;.astro&lt;/code&gt; template files by hand — that’s the only way to build with it. If I had compared a hand-coded EmDash site against an Elementor WordPress site, the comparison would have been unfairly stacked &lt;strong&gt;against&lt;/strong&gt; WordPress, because page builders add hundreds of milliseconds of overhead that have nothing to do with PHP, MySQL, or how WordPress actually serves a page. So I built the WordPress comparison theme the same way I built the EmDash one: a &lt;strong&gt;custom theme, written by hand&lt;/strong&gt;, no visual builder, no Elementor, no Bricks, no plugin soup pretending to be a CMS. Both sites are doing the identical job — rendering HTML from a hand-coded template against a database — using each platform’s native templating layer. The day someone ships a page-builder plugin for EmDash, I’ll happily re-run with both sides bloated equally. Until then, this is the apples-to-apples version.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“WordPress wasn’t optimized either.”&lt;/strong&gt; Correct. That was the point. Both backends were doing real work on every request. The numbers on optimized WP would be even better.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“You should have tested it on Node.js. There are no cold starts there.”&lt;/strong&gt; This is the most interesting objection so far, and it came from &lt;a href=&quot;https://maciekpalmowski.dev/&quot;&gt;Maciek Palmowski&lt;/a&gt; after he read the preview. He also pointed out that EmDash runs &lt;em&gt;fast&lt;/em&gt; on localhost in dev mode, which is the same observation from a different angle: take the edge round-trip out, and the engine flies. And he is right — EmDash is not strictly Workers-bound. Cloudflare’s own &lt;a href=&quot;https://blog.cloudflare.com/emdash-wordpress/&quot;&gt;EmDash launch post&lt;/a&gt; says explicitly: &lt;em&gt;“serverless, but you can run it on your own hardware or any platform you choose”&lt;/em&gt; and &lt;em&gt;“You can run EmDash anywhere, on any Node.js server.”&lt;/em&gt; On a long-running Node process with a local SQLite or Postgres, the cold-start problem disappears, the D1 round-trip latency disappears, and the engine almost certainly lands in WordPress-class TTFBs or better, given that V8 + JavaScript is a significantly leaner runtime than PHP-FPM. I have &lt;strong&gt;not&lt;/strong&gt; measured this. Two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;I don’t currently have a clean Node-hosting setup&lt;/strong&gt; to put it on for a proper apples-to-apples test against the same WordPress comparison site. That is fixable; I just haven’t fixed it. If anyone with a tidy Node host wants to re-run my benchmark scripts on their setup, the repos will be public — please go for it, and I will link your results in a follow-up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running EmDash on Node removes the headline feature.&lt;/strong&gt; That same Cloudflare launch post is titled &lt;em&gt;“Introducing EmDash — &lt;strong&gt;the spiritual successor to WordPress that solves plugin security&lt;/strong&gt;.”&lt;/em&gt; The “solves plugin security” claim depends on plugins executing inside their own “Dynamic Workers” sandboxes — the v8 isolate architecture and the workerd runtime. That sandboxing primitive is a Cloudflare Workers thing. On a vanilla Node deployment, you get the speed, but you lose the plugin isolation model that Cloudflare put in the post &lt;em&gt;title&lt;/em&gt;. The trade you are making is “my CMS runs fast but my plugins can do whatever they want to my server” — which is, structurally, the WordPress problem you were trying to escape in the first place.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So: yes, I would expect a Node-hosted EmDash to be fast — possibly faster than my hand-coded WordPress comparison. And I think that benchmark is genuinely interesting and should be run by someone with the right hardware. But it would be measuring a &lt;em&gt;different product&lt;/em&gt; than the one Cloudflare actually launched. The product Cloudflare launched is the one I tested: Workers + D1 + plugin isolation. That is the version of EmDash whose performance envelope this article is about.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“The geography helps WP.”&lt;/strong&gt; Actually it doesn’t — the WP traffic crosses ~400 km to Frankfurt while EmDash runs at the edge in (presumably) Warsaw. WordPress still wins by 6.4×. The longer path is dominated by 84 ms of fast PHP work.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WordPress is 6.4× faster&lt;/strong&gt; than EmDash on raw server processing for a small content site, because the database access pattern matters more than the runtime fashion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aggressive optimization closes the gap to ~4×&lt;/strong&gt;, but requires non-trivial developer effort and undocumented APIs. WordPress hits its number with zero tuning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Workers cold starts are real, daily-cycled, and exclusive to the &amp;gt;800 ms zone.&lt;/strong&gt; They matter most during business hours. They matter most for crawlers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Cloudflare paid Workers plan does not improve TTFB.&lt;/strong&gt; If anything, slightly worse in this test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge caching is a viable mitigation for human users&lt;/strong&gt;, not for Googlebot. The crawl-stats verdict from a blind expert review confirms this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EmDash is not currently a drop-in upgrade for the SMB-WordPress segment&lt;/strong&gt; I was hoping to address. The architecture is fundamentally promising; the current performance envelope is not where it needs to be for that audience.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I don’t need EmDash to &lt;em&gt;win&lt;/em&gt;. I want it to be a solid, credible alternative — the kind of thing I can put in front of a small-business client without a list of caveats. I still think the idea — Astro ergonomics, real CMS, no PHP — is worth pursuing. I just can’t recommend it today on the basis of “it will be faster than WordPress”, because the data says it won’t be.&lt;/p&gt;
&lt;h2 id=&quot;repos-and-discussion&quot;&gt;Repos and discussion&lt;/h2&gt;
&lt;p&gt;Both repositories are public so anyone can re-run the benchmark, push back on the methodology, or extend it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-emdash-cms-benchmark&quot;&gt;EmDash benchmark site&lt;/a&gt;&lt;/strong&gt; — full Astro 6 + EmDash + Cloudflare Workers + D1 setup.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; branch is the bare official &lt;code&gt;emdash-blog&lt;/code&gt; starter (the “before” baseline)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;perf/server-defer-widgets&lt;/code&gt; branch is the “faster” version&lt;/strong&gt; — contains the three optimization commits described above (&lt;code&gt;server:defer&lt;/code&gt; on widget areas, batched &lt;code&gt;getTermsForEntries&lt;/code&gt;, edge caching via Cloudflare Cache API). Includes wrapper components, skeleton fallbacks, middleware, and the Worker entry point wrapped with &lt;code&gt;caches.open()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/benchmark/&lt;/code&gt; folder contains &lt;code&gt;bench.sh&lt;/code&gt; (cron-scheduled curl collector), &lt;code&gt;analyze.py&lt;/code&gt; (percentile / distribution analysis), the full &lt;code&gt;report-v2.md&lt;/code&gt;, and raw &lt;code&gt;results.csv&lt;/code&gt; — everything you need to reproduce the numbers in this article.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;README&lt;/code&gt; also documents the gotchas I ran into along the way: &lt;code&gt;cacheHint&lt;/code&gt; emitted without a consumer, &lt;code&gt;server:defer&lt;/code&gt; not working on &lt;code&gt;node_modules&lt;/code&gt; components, Workers on custom domains bypassing CDN cache, and the architectural D1 latency floor.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/mateusz-zadorozny/shift64-wp-theme-emdash-flavor&quot;&gt;WordPress comparison theme (&lt;code&gt;emdash-flavor&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt; — hand-coded theme used as the control site. No page builder, no plugins beyond Object Cache, PHP 8.4 on Hetzner. Includes the seed content XML and the setup scripts that reproduce the exact comparison site from the &lt;code&gt;wp-cli&lt;/code&gt; up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the meantime, the right place to argue with me is X or LinkedIn. I’d genuinely like to be wrong about this — if you can show me the configuration that gets EmDash to WordPress-class numbers without manually rewriting half the data layer, I will publish the follow-up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discuss this article:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/matisportowiec/status/2044370462029869129&quot;&gt;On X&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/posts/mateusz-zadorozny_i-read-150-emdash-hot-takes-in-a-week-maybe-activity-7450135553863274496-PfCM&quot;&gt;On LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;em&gt;Both sites in this benchmark were scaffolded with Claude Code. The benchmark ran 182 randomized runs across 13 pages on each site over ~3.5 days — 4,732 measurements total — plus 30 follow-up runs after each optimization. Raw data, analysis scripts, and both site repos are linked above.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>emdash</category><category>wordpress</category><category>astro</category><category>cloudflare</category><category>ttfb</category><category>benchmark</category><category>performance</category></item><item><title>Your First Playwright Test for WooCommerce</title><link>https://shift64.com/blog/your-first-playwright-test-for-woocommerce</link><guid isPermaLink="true">https://shift64.com/blog/your-first-playwright-test-for-woocommerce</guid><description>Performance tests tell you how fast your store is. They don&apos;t tell you if your checkout actually works. Playwright does. Here&apos;s how to write your first test in under 10 minutes.</description><pubDate>Fri, 03 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;performance-is-worthless-if-the-store-is-broken&quot;&gt;Performance Is Worthless If the Store Is Broken&lt;/h2&gt;
&lt;p&gt;You optimized your TTFB. You tuned Redis. Your Lighthouse score is green across the board.&lt;/p&gt;
&lt;p&gt;Then a plugin update silently breaks the “Add to Cart” button. Nobody notices for three days — except your customers, who leave.&lt;/p&gt;
&lt;nav class=&quot;toc-component not-prose my-10 border-4 border-black bg-white p-6 shadow-[6px_6px_0px_0px_#000000]&quot;&gt; &lt;h2 class=&quot;mb-4 text-xl font-black font-heading uppercase tracking-tight&quot;&gt; Table of Contents &lt;/h2&gt; &lt;ol class=&quot;toc-list space-y-2&quot;&gt;&lt;/ol&gt; &lt;/nav&gt; &lt;script type=&quot;module&quot; src=&quot;https://shift64.com/opt/buildhome/repo/src/components/astro/TableOfContents.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;Performance tests answer one question: &lt;em&gt;how fast?&lt;/em&gt; But there’s a more fundamental question they completely ignore: &lt;strong&gt;does it work at all?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That’s what &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt; answers. It launches a real browser, walks through your store like a customer would, and tells you — in seconds — whether the critical paths still function. Add to cart. Checkout. Payment. The flows that directly convert to revenue.&lt;/p&gt;
&lt;p&gt;If you’re running a WooCommerce store and you’re not testing these flows automatically, you’re flying blind after every update.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-for-woocommerce-specifically&quot;&gt;Why This Matters for WooCommerce Specifically&lt;/h2&gt;
&lt;p&gt;WooCommerce is a plugin ecosystem stacked on top of WordPress. Every update — core, theme, or plugin — is a potential breaking change. And the failures are almost never loud. They’re silent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A gateway plugin update changes the checkout form markup. The page loads fine. The “Place Order” button does nothing.&lt;/li&gt;
&lt;li&gt;A theme update shifts a CSS class. Your cart popup stops appearing. The product page looks normal otherwise.&lt;/li&gt;
&lt;li&gt;A WooCommerce minor release changes how shipping methods render. No errors in the log. Customers just can’t select delivery.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You won’t catch these by looking at the admin dashboard. You won’t catch them with uptime monitoring. You catch them by simulating what a customer actually does — clicking through the store, adding products, and completing checkout.&lt;/p&gt;
&lt;p&gt;That’s exactly what Playwright does.&lt;/p&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;
&lt;h3 id=&quot;install-playwright&quot;&gt;Install Playwright&lt;/h3&gt;
&lt;p&gt;You need &lt;a href=&quot;https://nodejs.org/&quot;&gt;Node.js&lt;/a&gt; on your machine. Then:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; playwright@latest&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Accept the defaults. You’ll get a &lt;code&gt;tests/&lt;/code&gt; directory and a &lt;code&gt;playwright.config.ts&lt;/code&gt; file. That’s your entire setup.&lt;/p&gt;
&lt;h3 id=&quot;write-your-first-test&quot;&gt;Write Your First Test&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;tests/woo-add-to-cart.spec.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { test, expect } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;@playwright/test&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; STORE_URL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;https://yourstore.com&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Your store URL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;test&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;add product to cart and reach checkout&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;page&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 1. Open a product page&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; page.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;goto&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;STORE_URL&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/product/sample-product/`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 2. Handle the cookie banner — adjust the selector to match your plugin.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  //    Rejecting cookies keeps the test fast (no tracking scripts loaded).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; page.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getByRole&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;button&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, { name: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;Reject all&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;click&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 3. Find the &amp;quot;Add to Cart&amp;quot; button and verify it&amp;#39;s visible.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  //    If it doesn&amp;#39;t appear within 5 seconds, the test fails.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; addToCartButton&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; page.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;locator&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;button[name=&amp;quot;add-to-cart&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; expect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(addToCartButton).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toBeVisible&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; addToCartButton.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;click&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 4. WooCommerce shows a cart notification after adding a product.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  //    The exact markup depends on your theme — adjust the selector.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; viewCartButton&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; page.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;locator&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;#cart-popup a.wc-forward&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;first&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; expect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(viewCartButton).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toBeVisible&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; viewCartButton.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;click&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 5. Verify the product is in the cart — exactly 1 item.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; expect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(page.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;locator&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;.cart_item&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toHaveCount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 6. Verify the checkout button exists. If it&amp;#39;s missing,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  //    something broke in the cart template.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; checkoutButton&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; page.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;locator&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;.checkout-button&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; expect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(checkoutButton).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toBeVisible&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every line with &lt;code&gt;await&lt;/code&gt; tells Playwright: &lt;em&gt;finish this step before moving on&lt;/em&gt;. Every line with &lt;code&gt;expect&lt;/code&gt; is an assertion — if it fails, the test stops and tells you exactly what went wrong.&lt;/p&gt;
&lt;h3 id=&quot;run-it&quot;&gt;Run It&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; playwright&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; test&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. Playwright opens a browser, runs the scenario, and reports the result.&lt;/p&gt;
&lt;p&gt;Want to watch it happen in real time?&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; playwright&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; test&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --headed&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the test fails, Playwright saves a screenshot and a trace file. Open the report to step through what happened:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; playwright&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; show-report&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;when-to-run-tests&quot;&gt;When to Run Tests&lt;/h2&gt;
&lt;p&gt;You don’t need to run tests after every CSS tweak. But there are moments where skipping them is reckless:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WooCommerce core update&lt;/strong&gt; — The most common source of silent checkout breakage. New versions can change cart structure, checkout fields, or how payment gateways hook in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Theme update&lt;/strong&gt; — Themes control the layout and CSS selectors your tests rely on. A theme update that breaks a test might mean the store is broken — or it might mean you need to update a selector. Check manually first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugin install or update&lt;/strong&gt; — Especially payment, shipping, and cart plugins. &lt;a href=&quot;https://shift64.com/blog/staging-paradox-perfect-copy-ticking-time-bomb&quot;&gt;Plugin conflicts are WooCommerce’s greatest hit&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom code changes&lt;/strong&gt; — &lt;a href=&quot;https://shift64.com/services/custom-woocommerce-coding&quot;&gt;Custom WooCommerce code&lt;/a&gt;, template overrides, new hooks. Easy to break something without noticing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On a schedule&lt;/strong&gt; — Even without changes on your end, external services (payment gateways, courier APIs) can go down. A daily or hourly test run gives you early warning before customers start complaining.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For automation, plug your tests into CI/CD (GitHub Actions works well) or schedule a cron job that runs &lt;code&gt;npx playwright test&lt;/code&gt; at a set interval.&lt;/p&gt;
&lt;h2 id=&quot;what-to-test-next&quot;&gt;What to Test Next&lt;/h2&gt;
&lt;p&gt;One test is a good start. Here’s where to expand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Product search and filtering&lt;/strong&gt; — Does the search actually return results? Do category filters work?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customer login and order history&lt;/strong&gt; — Can returning customers access their accounts?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coupon codes&lt;/strong&gt; — Does the discount apply correctly?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple payment methods&lt;/strong&gt; — If you offer Stripe, PayPal, and bank transfer, test all three.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shipping method selection&lt;/strong&gt; — Can customers choose between delivery options?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;visual-regression&quot;&gt;Visual Regression&lt;/h3&gt;
&lt;p&gt;Playwright can also catch things no functional test will — layout shifts, broken styling, elements that moved after an update:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; expect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(page).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toHaveScreenshot&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;checkout-page.png&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This compares a screenshot against a baseline. If anything changes visually, the test flags it. Useful after theme updates when the store “works” but looks wrong.&lt;/p&gt;
&lt;h2 id=&quot;the-full-picture&quot;&gt;The Full Picture&lt;/h2&gt;
&lt;p&gt;Playwright tells you the store works. &lt;a href=&quot;https://shift64.com/services/woocommerce-care&quot;&gt;Constant performance engineering as part of WooCommerce Care&lt;/a&gt; tells you it stays fast. You need both.&lt;/p&gt;
&lt;p&gt;A store that loads in 400ms but has a broken checkout is worse than a store that loads in 2 seconds and actually converts. Start with one test. Run it after every update. Expand from there.&lt;/p&gt;
&lt;p&gt;If you don’t want to build and maintain test suites yourself — we offer &lt;a href=&quot;https://shift64.com/services/playwright-testing&quot;&gt;Playwright testing for WooCommerce&lt;/a&gt; as a service. Tests built around your specific store, your plugins, your custom logic. Not generic scripts. Real coverage for the flows that make you money.&lt;/p&gt;</content:encoded><category>woocommerce</category><category>playwright</category><category>testing</category><category>automation</category><category>devops</category></item><item><title>Fast WooCommerce Search: An Experiment with RediSearch and SHORTINIT</title><link>https://shift64.com/blog/fast-woocommerce-search-redisearch-shortinit</link><guid isPermaLink="true">https://shift64.com/blog/fast-woocommerce-search-redisearch-shortinit</guid><description>The default WooCommerce search is slow by design. I&apos;m building a custom plugin using RediSearch and SHORTINIT to see how fast autocomplete can get. Early results from a cheap VPS inside.</description><pubDate>Sat, 21 Mar 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WooCommerce search is slow. Not because someone wrote it badly — that’s just how WordPress works. Every keystroke in the search field boots the entire engine: plugins, themes, hooks, translations, sessions. By the time WordPress touches the database with your query, 100-300 milliseconds have already passed. The same problem affects &lt;a href=&quot;https://shift64.com/blog/woocommerce-checkout-slow-real-causes/&quot;&gt;checkout pages&lt;/a&gt; — it’s a systemic WordPress issue, not a search-specific one.&lt;/p&gt;
&lt;nav class=&quot;toc-component not-prose my-10 border-4 border-black bg-white p-6 shadow-[6px_6px_0px_0px_#000000]&quot;&gt; &lt;h2 class=&quot;mb-4 text-xl font-black font-heading uppercase tracking-tight&quot;&gt; Table of Contents &lt;/h2&gt; &lt;ol class=&quot;toc-list space-y-2&quot;&gt;&lt;/ol&gt; &lt;/nav&gt; &lt;script type=&quot;module&quot; src=&quot;https://shift64.com/opt/buildhome/repo/src/components/astro/TableOfContents.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;For autocomplete, where a user types a letter every 50-150 ms, that’s an eternity.&lt;/p&gt;
&lt;p&gt;I’ve been experimenting with a different approach: &lt;strong&gt;RediSearch as the search engine + SHORTINIT as an ultra-fast WordPress bootstrap&lt;/strong&gt;. A custom plugin, built from scratch. Early results on a cheap VPS: &lt;strong&gt;~54 ms median full round-trip&lt;/strong&gt;. Redis itself responds in under 10 ms.&lt;/p&gt;
&lt;p&gt;In this post I’ll walk through what I’ve built so far, how it works, and why I think it’s a direction worth exploring.&lt;/p&gt;
&lt;h2 id=&quot;why-is-the-default-woocommerce-search-slow&quot;&gt;Why Is the Default WooCommerce Search Slow?&lt;/h2&gt;
&lt;p&gt;Let’s start with &lt;strong&gt;what actually happens&lt;/strong&gt; when a customer types something into your store’s search field.&lt;/p&gt;
&lt;p&gt;A standard WordPress request looks roughly like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PHP loads &lt;code&gt;wp-config.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Loads &lt;strong&gt;every&lt;/strong&gt; active plugin (even the ones that have nothing to do with search)&lt;/li&gt;
&lt;li&gt;Loads the theme with all its hooks&lt;/li&gt;
&lt;li&gt;Initializes translations, sessions, permissions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only now&lt;/strong&gt; fires the SQL query to the database&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And that query? By default, WooCommerce searches post titles and content using &lt;code&gt;LIKE &amp;#39;%search_term%&amp;#39;&lt;/code&gt;. MySQL can’t optimize this — it has to scan every row in the table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; 30-50 MB of RAM and hundreds of milliseconds before your customer sees the first suggestion.&lt;/p&gt;
&lt;p&gt;Plugins like FiboSearch or SearchWP solve part of this problem (FiboSearch Pro uses SHORTINIT internally), but none of the popular plugins use RediSearch as a full-text search engine.&lt;/p&gt;
&lt;h2 id=&quot;redisearch--what-it-is-and-why-it-matters&quot;&gt;RediSearch — What It Is and Why It Matters&lt;/h2&gt;
&lt;p&gt;Redis is a key-value database that keeps everything in RAM. It’s commonly used in WordPress as a cache for &lt;code&gt;wp_options&lt;/code&gt; and sessions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RediSearch&lt;/strong&gt; is a Redis module that adds full-text search capabilities. Since Redis Stack (7.2+), it ships bundled with Redis — but it’s still a module, not a separate database. It enables indexing, prefix matching, and typo tolerance on top of data you already keep in Redis.&lt;/p&gt;
&lt;p&gt;Why does this matter? Because Redis operates on RAM and responds in microseconds. In my test on an index of 2,500 WooCommerce products, a typical &lt;code&gt;FT.SEARCH&lt;/code&gt; response time is &lt;strong&gt;2-5 ms per query&lt;/strong&gt;. Practically always under 10 ms.&lt;/p&gt;
&lt;p&gt;Compare that to MySQL &lt;code&gt;LIKE&lt;/code&gt;, which on the same product count can take 50-200 ms.&lt;/p&gt;
&lt;h2 id=&quot;shortinit--wordpress-without-wordpress&quot;&gt;SHORTINIT — WordPress Without WordPress&lt;/h2&gt;
&lt;p&gt;This is where it gets interesting.&lt;/p&gt;
&lt;p&gt;WordPress has a built-in constant called &lt;code&gt;SHORTINIT&lt;/code&gt; that very few people know about. When you set it to &lt;code&gt;true&lt;/code&gt; before loading &lt;code&gt;wp-settings.php&lt;/code&gt;, WordPress loads &lt;strong&gt;the bare minimum&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wp-config.php&lt;/code&gt; (database credentials)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$wpdb&lt;/code&gt; (database object)&lt;/li&gt;
&lt;li&gt;Core constants (&lt;code&gt;ABSPATH&lt;/code&gt;, &lt;code&gt;WPINC&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s it. &lt;strong&gt;No plugins. No themes. No hooks. No translations.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of 30-50 MB RAM and 100-300 ms of bootstrap, you get ~5-10 MB and under 5 ms to the first line of your logic. The autocomplete endpoint starts faster than WordPress would take to load the list of active plugins.&lt;/p&gt;
&lt;h3 id=&quot;the-catch&quot;&gt;The Catch&lt;/h3&gt;
&lt;p&gt;SHORTINIT means no access to &lt;code&gt;wp_options&lt;/code&gt;, &lt;code&gt;get_option()&lt;/code&gt;, or virtually any WordPress function beyond the raw &lt;code&gt;$wpdb&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;How do I deal with this? Plugin settings (Redis host, port, limits, search strategy) are saved to an auto-generated PHP file as &lt;code&gt;define()&lt;/code&gt; constants. The config class detects which mode it booted in and picks one of two separate paths: if the &lt;code&gt;MERIDA_SEARCH_REDIS_HOST&lt;/code&gt; constant exists, we’re in SHORTINIT and all config comes from constants (early return). Only when the constants aren’t there does it check whether &lt;code&gt;get_option()&lt;/code&gt; even exists, and then reads from the database.&lt;/p&gt;
&lt;p&gt;One or the other — never a mix. Unconventional, but it works surprisingly well.&lt;/p&gt;
&lt;h2 id=&quot;early-results-from-a-cheap-vps&quot;&gt;Early Results from a Cheap VPS&lt;/h2&gt;
&lt;p&gt;I tested everything on a cheap VPS with a ~2 GHz processor. Deliberately not on a rocket — I wanted to see what this looks like under realistic conditions for a store that doesn’t spend a fortune on hosting. As I covered in &lt;a href=&quot;https://shift64.com/blog/why-hosting-should-be-step-one-in-wordpress-optimization/&quot;&gt;why hosting should be step one&lt;/a&gt;, server quality sets the baseline for everything else.&lt;/p&gt;
&lt;h3 id=&quot;first-observations&quot;&gt;First Observations&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; these are sample requests from DevTools, not a full benchmark. Proper performance testing (including &lt;a href=&quot;https://shift64.com/lessons/lesson-3-how-to-measure-backend-performance/&quot;&gt;proper backend measurement methodology&lt;/a&gt;) is planned after confirming usability — meaning after I verify that search results actually make sense to the user. For now, I’m testing whether the architecture holds up.&lt;/p&gt;
&lt;p&gt;What I’m seeing at this stage: median autocomplete response time (full cycle: client, server, Redis, response) sits around &lt;strong&gt;50-60 ms&lt;/strong&gt;. ~80% of requests come in under 90 ms. Redis itself responds practically always under 10 ms — the bottleneck is the network, not the application.&lt;/p&gt;
&lt;h2 id=&quot;three-ideas-that-made-a-difference&quot;&gt;Three Ideas That Made a Difference&lt;/h2&gt;
&lt;p&gt;I don’t want to turn this into a full tutorial (not yet), but a few concepts are worth flagging — they might be useful to you even in a different context.&lt;/p&gt;
&lt;h3 id=&quot;precise-first-loose-later&quot;&gt;Precise First, Loose Later&lt;/h3&gt;
&lt;p&gt;Instead of immediately firing fuzzy search (which is slower and too liberal — “soap” fuzzy-matches “soup”), I use three passes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Exact prefix&lt;/strong&gt; — &lt;code&gt;dryer*&lt;/code&gt; — match words starting with the typed phrase. Fastest and most accurate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query trimming&lt;/strong&gt; — remove filler words (“to”, “for”, “the”, “a”) and search again&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Typo tolerance&lt;/strong&gt; — &lt;code&gt;%dryer%&lt;/code&gt; — allows minor spelling mistakes, fires ONLY when the previous passes returned nothing&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key rule: if exact matching returns results, typo tolerance never fires. Zero wasted work.&lt;/p&gt;
&lt;h3 id=&quot;diacritics-normalization&quot;&gt;Diacritics Normalization&lt;/h3&gt;
&lt;p&gt;Product titles are indexed twice: with diacritics and without. A user typing “rak” matches “rąk” (Polish for “hands”). In any e-commerce market with non-ASCII characters, this is essential — customers routinely skip special characters when searching.&lt;/p&gt;
&lt;h3 id=&quot;re-ranking-results-after-redis&quot;&gt;Re-Ranking Results After Redis&lt;/h3&gt;
&lt;p&gt;RediSearch sorts results by how well they match the query textually. But it knows nothing about the store’s context. So after Redis responds, PHP applies corrections: an exact SKU hit goes to the top, out-of-stock products drop in ranking, promoted products rank higher, and titles starting with the search phrase get a boost.&lt;/p&gt;
&lt;p&gt;Redis returns 5x more candidates than needed, PHP recalculates scores and trims to the limit.&lt;/p&gt;
&lt;h2 id=&quot;when-does-this-make-sense&quot;&gt;When Does This Make Sense?&lt;/h2&gt;
&lt;p&gt;Let’s be honest — &lt;strong&gt;for 90% of WooCommerce stores, this is overkill&lt;/strong&gt;. If you have 200 products and a few dozen daily visitors, FiboSearch (free or pro) will do the job.&lt;/p&gt;
&lt;p&gt;This approach starts making sense when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have thousands of products and want sub-100 ms round-trip search&lt;/li&gt;
&lt;li&gt;You have &lt;strong&gt;your own server&lt;/strong&gt; (VPS or dedicated) where you can install the RediSearch module&lt;/li&gt;
&lt;li&gt;You want full control over search logic (synonyms, field weights, re-ranking)&lt;/li&gt;
&lt;li&gt;You care about resource efficiency — the autocomplete endpoint uses ~5-10 MB RAM instead of 30-50 MB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On shared hosting this will be difficult — most providers don’t offer Redis with the Search module, and even if Redis is available, loading custom modules is usually not an option. A VPS or dedicated server gives you full control here.&lt;/p&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h2&gt;
&lt;p&gt;This is just the beginning. What’s planned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thorough testing of search result quality — because speed without accuracy is pointless&lt;/li&gt;
&lt;li&gt;Load testing under heavy traffic (many concurrent users, larger indexes)&lt;/li&gt;
&lt;li&gt;If the plugin goes to production — a full case study with real data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If this caught your attention, have questions, or want to talk WooCommerce — find me on &lt;a href=&quot;https://www.linkedin.com/in/mateusz-zadorozny/&quot; target=&quot;_blank&quot;&gt;LinkedIn&lt;/a&gt;. You can also leave a comment below.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;At &lt;a href=&quot;https://shift64.com/&quot;&gt;SHIFT64&lt;/a&gt; we engineer WooCommerce performance for a living — from &lt;a href=&quot;https://shift64.com/services/woocommerce-care/&quot;&gt;ongoing care and monitoring&lt;/a&gt; to custom solutions like this one. If your store could be faster — &lt;a href=&quot;https://shift64.com/#start&quot;&gt;let’s talk&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>woocommerce</category><category>performance</category><category>search</category><category>redis</category><category>wordpress</category></item><item><title>Why Your WooCommerce Checkout Is Slow (And It&apos;s Not What You Think)</title><link>https://shift64.com/blog/woocommerce-checkout-slow-real-causes</link><guid isPermaLink="true">https://shift64.com/blog/woocommerce-checkout-slow-real-causes</guid><description>Your checkout page is the only page that can&apos;t be cached. Every millisecond of latency runs at bare server speed — and it&apos;s costing you conversions. Here&apos;s what&apos;s actually slow and how to fix it.</description><pubDate>Thu, 05 Mar 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;why-your-woocommerce-checkout-is-slow-and-its-not-what-you-think&quot;&gt;Why Your WooCommerce Checkout Is Slow (And It’s Not What You Think)&lt;/h1&gt;
&lt;h2 id=&quot;the-page-you-cant-cache&quot;&gt;The Page You Can’t Cache&lt;/h2&gt;
&lt;p&gt;Every speed optimization guide starts the same way: install a caching plugin, enable a CDN, compress your images. And for 90% of your pages, that works. Homepage? Cached. Category pages? Cached. Product pages? Mostly cached.&lt;/p&gt;
&lt;nav class=&quot;toc-component not-prose my-10 border-4 border-black bg-white p-6 shadow-[6px_6px_0px_0px_#000000]&quot;&gt; &lt;h2 class=&quot;mb-4 text-xl font-black font-heading uppercase tracking-tight&quot;&gt; Table of Contents &lt;/h2&gt; &lt;ol class=&quot;toc-list space-y-2&quot;&gt;&lt;/ol&gt; &lt;/nav&gt; &lt;script type=&quot;module&quot; src=&quot;https://shift64.com/opt/buildhome/repo/src/components/astro/TableOfContents.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;But your checkout page is different.&lt;/p&gt;
&lt;p&gt;Checkout contains user-specific data — cart contents, shipping address, tax calculations, payment tokens. No caching plugin can touch it. Every single checkout load runs the full PHP execution pipeline, hits the database, calls external APIs, and renders the result from scratch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your checkout page runs at bare server speed. Always.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That’s why a store can score 95 on Lighthouse and still have an 8-second checkout. The cache hides the problem everywhere else. Checkout exposes it.&lt;/p&gt;
&lt;p&gt;And here’s the business case: Google and Deloitte’s “Milliseconds Make Millions” study found that a 100ms improvement in page load time produces an &lt;strong&gt;8.4% increase in retail conversions&lt;/strong&gt;. On checkout — the page where purchase decisions happen — every millisecond of latency is directly correlated with lost revenue.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;how-bad-is-it-lets-look-at-the-numbers&quot;&gt;How Bad Is It? Let’s Look at the Numbers&lt;/h2&gt;
&lt;p&gt;One Reddit user ran a clean test: fresh WordPress install, default Twenty Twenty-Four theme, &lt;strong&gt;all plugins deactivated except WooCommerce itself&lt;/strong&gt;. The &lt;code&gt;wc-ajax=update_order_review&lt;/code&gt; call — the AJAX request that fires every time the checkout page updates — took &lt;strong&gt;700-1100ms&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That’s nearly a full second of server response time with zero plugins, zero customization, and a stock theme. The overhead is coming from WooCommerce itself: stock checks, shipping calculations, tax logic, payment gateway validation. All of it runs on every checkout page load — even if you don’t use half of those features.&lt;/p&gt;
&lt;p&gt;Now add what a real store looks like:&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;/th&gt;&lt;th&gt;Latency added&lt;/th&gt;&lt;th&gt;Source&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;WooCommerce core checkout logic&lt;/td&gt;&lt;td&gt;700-1100ms&lt;/td&gt;&lt;td&gt;Reddit benchmark, minimal setup&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Stripe payment gateway&lt;/td&gt;&lt;td&gt;+1000-2500ms&lt;/td&gt;&lt;td&gt;GitHub issue #1261, 3.5x slowdown vs. cash-on-delivery&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Real-time shipping rates (USPS/UPS API)&lt;/td&gt;&lt;td&gt;+200-800ms&lt;/td&gt;&lt;td&gt;External API round-trip&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tax calculation (Avalara)&lt;/td&gt;&lt;td&gt;+100-500ms&lt;/td&gt;&lt;td&gt;External API round-trip&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;reCAPTCHA validation&lt;/td&gt;&lt;td&gt;+100-300ms&lt;/td&gt;&lt;td&gt;Google API round-trip&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Order Attribution Tracking&lt;/td&gt;&lt;td&gt;+50-200ms&lt;/td&gt;&lt;td&gt;WooCommerce default since 8.5&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Add those up. A “normal” WooCommerce checkout with Stripe, one shipping API, and a tax service is doing &lt;strong&gt;2-5 seconds of server-side processing&lt;/strong&gt; before the browser even starts rendering the response. And this is before we talk about frontend JavaScript.&lt;/p&gt;
&lt;p&gt;One developer on r/woocommerce summed it up: &lt;em&gt;“What’s the #1 thing that slows down your WooCommerce store?”&lt;/em&gt; — The top answer, with the most upvotes: &lt;em&gt;“It’s WooCommerce itself.”&lt;/em&gt;&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;the-four-layers-of-checkout-latency&quot;&gt;The Four Layers of Checkout Latency&lt;/h2&gt;
&lt;p&gt;Most guides treat checkout slowness as a single problem. It’s not. There are four distinct layers, and you need to diagnose which one is killing you before you can fix it.&lt;/p&gt;
&lt;h3 id=&quot;layer-1-the-server&quot;&gt;Layer 1: The Server&lt;/h3&gt;
&lt;p&gt;PHP is single-threaded. When a customer clicks “Place Order,” WooCommerce processes the entire checkout sequence — validate cart, calculate taxes, create order, call payment gateway — on &lt;strong&gt;one CPU core, in one line&lt;/strong&gt;. It can’t split this across multiple cores. It can’t parallelize.&lt;/p&gt;
&lt;p&gt;This means the clock speed of that single core determines your checkout speed. A server with 8 cores at 2.3 GHz will process checkout slower than a server with 2 cores at 5.0 GHz. More cores handle more concurrent requests — but each individual checkout runs only as fast as one core can go.&lt;/p&gt;
&lt;p&gt;Most managed hosting providers run older server-grade processors (Intel Xeon, AMD EPYC) at 2.0-2.5 GHz. These are designed for density — packing thousands of tenants onto shared hardware — not for single-threaded speed. A high-frequency AMD Ryzen 9 at 5.0+ GHz processes the same PHP code roughly &lt;strong&gt;twice as fast&lt;/strong&gt; per request. That’s the difference between a 1.5-second checkout and a 0.75-second checkout. Physics, not software.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to check your CPU:&lt;/strong&gt; SSH into your server and run:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;lscpu&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; grep&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;Model name\|MHz&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see “AMD EPYC 7xx1” at 2.0 GHz on a shared host — that’s a hardware ceiling no plugin can overcome.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPcache is critical for checkout.&lt;/strong&gt; OPcache stores pre-compiled PHP bytecode in memory so PHP doesn’t recompile scripts on every request. On cached pages, this barely matters — the page is served from HTML cache anyway. On checkout, it matters enormously, because every request runs the full PHP stack.&lt;/p&gt;
&lt;p&gt;One Reddit user discovered that two identical WooCommerce stores on the same hosting had drastically different checkout speeds. The difference? OPcache wasn’t enabled after a PHP version upgrade on the slower one. &lt;em&gt;“This made such a big difference on AJAX functions (eg add to cart etc).”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Recommended minimum:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;opcache.memory_consumption&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=256&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;opcache.max_accelerated_files&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=20000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;opcache.revalidate_freq&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=60&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;layer-2-the-database&quot;&gt;Layer 2: The Database&lt;/h3&gt;
&lt;p&gt;Every checkout triggers dozens of database queries. The two biggest offenders:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The autoloaded options problem.&lt;/strong&gt; On every page load (including checkout), WordPress runs:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; option_name, option_value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wp_options&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; autoload &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;yes&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This loads every autoloaded option into memory. On a fresh WordPress install, that’s a few hundred KB. On a mature WooCommerce store with 30+ plugins, it can exceed 10MB. And here’s the real problem: the &lt;code&gt;autoload&lt;/code&gt; column has &lt;strong&gt;no index by default&lt;/strong&gt;. On a wp_options table with 50,000+ rows, this single query can take hundreds of milliseconds.&lt;/p&gt;
&lt;p&gt;Check your autoload size:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; SUM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;LENGTH&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(option_value)) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; autoload_bytes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wp_options&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; autoload &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;yes&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it’s over 1MB, you have a problem. Over 5MB, it’s an emergency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The postmeta bloat problem (legacy order storage).&lt;/strong&gt; If you’re still using WooCommerce’s legacy order storage, every order creates 1 row in &lt;code&gt;wp_posts&lt;/code&gt; and &lt;strong&gt;~29 rows in &lt;code&gt;wp_postmeta&lt;/code&gt;&lt;/strong&gt;. A store with 50,000 orders has over 1.4 million rows in postmeta. The JOIN queries that WooCommerce runs during checkout slow to a crawl.&lt;/p&gt;
&lt;p&gt;The fix: migrate to &lt;strong&gt;High-Performance Order Storage (HPOS)&lt;/strong&gt;. HPOS replaces the wp_posts/wp_postmeta model with optimized dedicated tables. Benchmarks show:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;5x faster&lt;/strong&gt; order creation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1.5x faster&lt;/strong&gt; checkout throughput under concurrent load&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;40x faster&lt;/strong&gt; admin order filtering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you haven’t migrated yet, this is likely your single biggest database bottleneck.&lt;/p&gt;
&lt;h3 id=&quot;layer-3-external-apis-the-silent-killer&quot;&gt;Layer 3: External APIs (The Silent Killer)&lt;/h3&gt;
&lt;p&gt;This is the layer most optimization guides miss entirely.&lt;/p&gt;
&lt;p&gt;During a typical checkout, WooCommerce makes &lt;strong&gt;multiple blocking HTTP calls&lt;/strong&gt; to external services:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Payment gateway&lt;/strong&gt; — Stripe processes the charge (1-3 seconds round-trip)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shipping rates&lt;/strong&gt; — USPS/UPS/FedEx APIs return real-time shipping options (200-800ms)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tax calculation&lt;/strong&gt; — TaxJar or Avalara calculates tax in real-time (100-500ms)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Address validation&lt;/strong&gt; — Some setups validate the shipping address against external databases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fraud prevention&lt;/strong&gt; — reCAPTCHA or similar services verify the customer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of these is a synchronous HTTP call. PHP waits for each response before continuing. They don’t run in parallel — they stack.&lt;/p&gt;
&lt;p&gt;The compounding is brutal. Take a checkout with Stripe + USPS real-time rates + Avalara tax + reCAPTCHA:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Stripe API:     ████████████████ 1800ms&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;USPS API:       ██████████       500ms&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Avalara API:    ████████         400ms&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;reCAPTCHA:      ████             200ms&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                 ─────&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Total blocking:                  2900ms&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s nearly &lt;strong&gt;3 seconds of waiting&lt;/strong&gt; — and none of it is your server’s fault. Your PHP code is literally idle, waiting for external services to respond. No amount of server optimization will fix this. You need to either reduce the number of external calls or find ways to make them non-blocking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to find hidden API calls:&lt;/strong&gt; Install Query Monitor and check the HTTP API Calls panel on your checkout page. You may discover plugins making calls to dead endpoints — one developer found that old, forgotten plugins were making API calls to services that no longer existed, and the &lt;strong&gt;timeout on those calls&lt;/strong&gt; was adding 5-10 seconds per request.&lt;/p&gt;
&lt;h3 id=&quot;layer-4-the-frontend&quot;&gt;Layer 4: The Frontend&lt;/h3&gt;
&lt;p&gt;After the server finishes processing, the browser still needs to render the checkout page. This is where payment gateway JavaScript creates problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stripe’s JavaScript overhead:&lt;/strong&gt; The official Stripe plugin loads 3 stylesheets globally, injects scripts on product pages (not just checkout), creates an analytics iframe, and sets cookies — all without regard for consent APIs. One developer documented that after simply enabling the Stripe plugin, the checkout JS payload increased significantly and the payment buttons took &lt;strong&gt;10-15 seconds to render&lt;/strong&gt; on a budget host.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The checkout block paradox:&lt;/strong&gt; WooCommerce’s newer block-based checkout loads approximately 300 KB of compressed JavaScript — roughly 3x larger than the classic shortcode checkout. The request count nearly doubles. However, WooCommerce’s own data shows a &lt;strong&gt;27% increase in checkout conversions&lt;/strong&gt; from the block checkout due to better UX. Faster isn’t always better if the UX is worse.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Theme overhead on checkout:&lt;/strong&gt; Multipurpose themes (Flatsome, Woodmart, Divi) load their entire CSS/JS stack on every page — including checkout. Sliders, animations, megamenu scripts, font libraries. None of it is needed on checkout. One developer reported that switching from a commercial theme to a custom lightweight theme cut their checkout page load from 6 seconds to 2.8 seconds.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;how-to-diagnose-your-specific-bottleneck&quot;&gt;How to Diagnose Your Specific Bottleneck&lt;/h2&gt;
&lt;p&gt;Don’t guess. Measure.&lt;/p&gt;
&lt;h3 id=&quot;step-1-isolate-server-response-time&quot;&gt;Step 1: Isolate server response time&lt;/h3&gt;
&lt;p&gt;Open your browser DevTools, go to your checkout page, and find the main document request in the Network tab. Look at the TTFB (Time to First Byte).&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;TTFB&lt;/th&gt;&lt;th&gt;Diagnosis&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&amp;lt; 200ms&lt;/td&gt;&lt;td&gt;Excellent. Your server isn’t the problem.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;200-500ms&lt;/td&gt;&lt;td&gt;Acceptable. Room for optimization but not critical.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;500-1000ms&lt;/td&gt;&lt;td&gt;Slow. Server-side investigation needed.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&amp;gt; 1000ms&lt;/td&gt;&lt;td&gt;Your server is the primary bottleneck.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;step-2-install-query-monitor&quot;&gt;Step 2: Install Query Monitor&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://wordpress.org/plugins/query-monitor/&quot;&gt;Query Monitor&lt;/a&gt; is the single most useful diagnostic tool for WooCommerce performance. On your checkout page, check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Queries by Caller&lt;/strong&gt; — which plugins generate the most database queries?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slow Queries&lt;/strong&gt; — any query over 50ms is a red flag&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP API Calls&lt;/strong&gt; — which external APIs are being called, and how long each takes?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Autoloaded Options&lt;/strong&gt; — how much data is being autoloaded?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-3-check-the-wc-ajaxupdate_order_review-call&quot;&gt;Step 3: Check the wc-ajax=update_order_review call&lt;/h3&gt;
&lt;p&gt;This AJAX request fires every time the checkout page updates (changing shipping method, entering an address). Open Network tab, filter by “update_order_review,” and watch its timing. This is the purest measure of your checkout’s server-side performance.&lt;/p&gt;
&lt;h3 id=&quot;step-4-test-with-a-baseline&quot;&gt;Step 4: Test with a baseline&lt;/h3&gt;
&lt;p&gt;Create a simple PHP file on your server:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;lt;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; echo&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; microtime&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;); &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Measure its TTFB. If even this takes 500ms+, your server itself is the bottleneck — before WordPress, before WooCommerce, before any plugin. This tells you whether the problem is infrastructure or application.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;what-actually-works-not-install-a-caching-plugin&quot;&gt;What Actually Works (Not “Install a Caching Plugin”)&lt;/h2&gt;
&lt;p&gt;Based on the diagnosis above, here’s what to fix — in order of impact.&lt;/p&gt;
&lt;h3 id=&quot;if-your-ttfb-is-over-1-second-fix-the-server-first&quot;&gt;If your TTFB is over 1 second: fix the server first&lt;/h3&gt;
&lt;p&gt;No plugin can overcome slow hardware. If your server runs at 2.0 GHz on shared infrastructure, the checkout will be slow regardless of what you optimize. The single highest-impact change is moving to dedicated high-frequency CPU. We’ve benchmarked WooCommerce checkouts on AMD Ryzen 9 (5.0+ GHz) vs standard Xeon hosts — the difference is typically 40-60% faster server response time, which translates directly to checkout speed.&lt;/p&gt;
&lt;h3 id=&quot;if-query-monitor-shows-200-database-queries-fix-the-database&quot;&gt;If Query Monitor shows 200+ database queries: fix the database&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Migrate to HPOS&lt;/strong&gt; if you haven’t. 5x faster order creation is not a marginal improvement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit autoloaded options.&lt;/strong&gt; Disable autoload on everything that isn’t needed on every page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean orphaned data.&lt;/strong&gt; &lt;code&gt;_wc_session_&lt;/code&gt; entries, transients, revision postmeta — these accumulate silently.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;if-external-api-calls-dominate-reduce-blocking-calls&quot;&gt;If external API calls dominate: reduce blocking calls&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use flat-rate or table-rate shipping&lt;/strong&gt; instead of real-time carrier APIs where possible. Real-time rates look nice but add 200-800ms per checkout. If your shipping is predictable, pre-calculate it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluate your tax solution.&lt;/strong&gt; TaxJar responds in under 20ms. Avalara can take hundreds of milliseconds. If you’re using Avalara, consider whether the additional complexity is worth the latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit payment gateway plugins.&lt;/strong&gt; Run checkout with each gateway individually and compare timing. Stripe’s documented 3.5x overhead is gateway-specific — alternatives may be faster for your use case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove dead endpoints.&lt;/strong&gt; Query Monitor’s HTTP API panel reveals plugins calling APIs that no longer exist. The timeout on those calls can add 5-30 seconds.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;if-frontend-javascript-is-the-bottleneck-audit-what-loads-on-checkout&quot;&gt;If frontend JavaScript is the bottleneck: audit what loads on checkout&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Disable plugins per page.&lt;/strong&gt; Tools like Asset CleanUp or Perfmatters let you prevent non-essential plugins from loading CSS/JS on checkout. Your countdown timer, your popup plugin, your slider — none of them belong on checkout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluate the checkout block vs shortcode.&lt;/strong&gt; Block checkout is heavier (3x JS) but converts 27% better. Test both and measure conversion rate, not just page speed.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;the-uncomfortable-truth&quot;&gt;The Uncomfortable Truth&lt;/h2&gt;
&lt;p&gt;Most WooCommerce speed guides are written by hosting companies or plugin vendors. Their advice naturally gravitates toward what they sell — “upgrade your hosting plan” or “install our optimization plugin.”&lt;/p&gt;
&lt;p&gt;The reality is more nuanced. Checkout latency is a compounding problem across four distinct layers, and the fix depends entirely on which layer is your bottleneck. A caching plugin won’t help because checkout can’t be cached. A faster CDN won’t help because the latency is server-side. And “disabling plugins” is a blunt instrument that often breaks functionality you need.&lt;/p&gt;
&lt;p&gt;The approach that actually works: &lt;strong&gt;measure each layer independently, identify the dominant bottleneck, and fix that first.&lt;/strong&gt; Then move to the next layer.&lt;/p&gt;
&lt;p&gt;Every update we deploy through &lt;a href=&quot;https://shift64.com/services/woocommerce-care&quot;&gt;WooCommerce Care&lt;/a&gt; goes through this exact diagnostic process. We profile the checkout, identify where the milliseconds are hiding, and eliminate them layer by layer — server tuning, database optimization, API audit, frontend cleanup. Then we verify with &lt;a href=&quot;https://shift64.com/services/playwright-testing&quot;&gt;automated Playwright tests&lt;/a&gt; that the entire purchase flow still works end-to-end.&lt;/p&gt;
&lt;p&gt;If your checkout takes more than 2 seconds and you’re not sure where the latency lives — &lt;a href=&quot;https://shift64.com/#start&quot;&gt;that’s exactly what we diagnose&lt;/a&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;checkout-speed-diagnostic-checklist&quot;&gt;Checkout Speed Diagnostic Checklist&lt;/h2&gt;
&lt;p&gt;Before optimizing, measure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Checkout TTFB (browser DevTools &amp;gt; Network &amp;gt; main document)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc-ajax=update_order_review&lt;/code&gt; response time&lt;/li&gt;
&lt;li&gt;Total database queries on checkout (Query Monitor)&lt;/li&gt;
&lt;li&gt;Autoloaded data size (SQL query above)&lt;/li&gt;
&lt;li&gt;External HTTP API calls and their timing (Query Monitor)&lt;/li&gt;
&lt;li&gt;JavaScript payload size on checkout page&lt;/li&gt;
&lt;li&gt;PHP baseline TTFB (test.php with just &lt;code&gt;echo microtime()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CPU model and clock speed (&lt;code&gt;lscpu&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;OPcache status (&lt;code&gt;php -i | grep opcache&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;HPOS migration status (WooCommerce &amp;gt; Settings &amp;gt; Advanced &amp;gt; Features)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>woocommerce</category><category>performance</category><category>checkout</category><category>optimization</category><category>hosting</category></item><item><title>The Staging Paradox: Why a Perfect Copy of Production Is a Ticking Time Bomb</title><link>https://shift64.com/blog/staging-paradox-perfect-copy-ticking-time-bomb</link><guid isPermaLink="true">https://shift64.com/blog/staging-paradox-perfect-copy-ticking-time-bomb</guid><description>A 1:1 database clone sends real emails, charges real cards, and corrupts your analytics. Here&apos;s how to build staging environments that are functionally identical but externally dead.</description><pubDate>Wed, 04 Mar 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;the-staging-paradox-why-a-perfect-copy-of-production-is-a-ticking-time-bomb&quot;&gt;The Staging Paradox: Why a Perfect Copy of Production Is a Ticking Time Bomb&lt;/h1&gt;
&lt;h2 id=&quot;the-mantra-everyone-gets-wrong&quot;&gt;The Mantra Everyone Gets Wrong&lt;/h2&gt;
&lt;p&gt;“Never test in production.”&lt;/p&gt;
&lt;p&gt;Every WordPress developer knows it. So we clone the production database to a staging domain, check that the site loads, and move on with our day. Responsible engineering. Problem solved.&lt;/p&gt;
&lt;nav class=&quot;toc-component not-prose my-10 border-4 border-black bg-white p-6 shadow-[6px_6px_0px_0px_#000000]&quot;&gt; &lt;h2 class=&quot;mb-4 text-xl font-black font-heading uppercase tracking-tight&quot;&gt; Table of Contents &lt;/h2&gt; &lt;ol class=&quot;toc-list space-y-2&quot;&gt;&lt;/ol&gt; &lt;/nav&gt; &lt;script type=&quot;module&quot; src=&quot;https://shift64.com/opt/buildhome/repo/src/components/astro/TableOfContents.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;Except that staging site just sent 800 abandoned cart emails to real customers. With discount codes. For carts they already purchased.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A perfect 1:1 copy of production is not a safe testing environment. It’s production running on a different domain.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every cron job, every API key, every webhook endpoint, every sending queue - it all came along in the database dump. And unless you actively neutralize every one of those connections, staging is just production with a false sense of security.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;what-went-wrong-a-real-scenario&quot;&gt;What Went Wrong: A Real Scenario&lt;/h2&gt;
&lt;p&gt;Here’s what a typical WooCommerce staging disaster looks like.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The store:&lt;/strong&gt; WooCommerce with ShopMagic for abandoned cart recovery, Stripe for payments, GA4 tracking conversions, and webhook integrations to a fulfillment center.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What the developer did:&lt;/strong&gt; Cloned the database. Checked that the homepage loads. Started working.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happened within 24 hours:&lt;/strong&gt;&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Event&lt;/th&gt;&lt;th&gt;Business impact&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;ShopMagic’s abandoned cart cron job fired&lt;/td&gt;&lt;td&gt;Hundreds of customers received “You forgot something!” emails with discount codes for carts they’d already purchased&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GA4 kept tracking&lt;/td&gt;&lt;td&gt;Conversion data polluted with test transactions, skewing ROAS calculations for weeks&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Webhook fired on test order&lt;/td&gt;&lt;td&gt;Fulfillment center received and shipped a test order to a real address&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Dozens of customer complaints. Corrupted analytics. Damaged trust.&lt;/p&gt;
&lt;p&gt;All from a “safe” staging environment.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;the-contradiction-at-the-core-of-staging&quot;&gt;The Contradiction at the Core of Staging&lt;/h2&gt;
&lt;p&gt;A useful staging environment has to satisfy three requirements that directly conflict with each other:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Functional parity.&lt;/strong&gt; You need the same code, plugins, themes, and configurations that run in production. Staging with deactivated plugins or different versions gives you false confidence - you’re testing something that doesn’t exist in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Data isolation.&lt;/strong&gt; No data can escape to external systems. No emails to customers, no analytics events, no webhook calls to production endpoints, no real payment processing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Sandbox mode.&lt;/strong&gt; Where full isolation isn’t possible (payment gateways, external APIs), you need test credentials that mirror production behavior without real-world consequences.&lt;/p&gt;
&lt;p&gt;The conflict is obvious: you want ShopMagic active so you can test abandoned cart recovery, but you can’t have it emailing hundreds of real customers about carts they already purchased.&lt;/p&gt;
&lt;p&gt;Most developers resolve this conflict by breaking requirement #1 - they deactivate plugins. That’s the wrong answer.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;five-ways-developers-get-this-wrong&quot;&gt;Five Ways Developers Get This Wrong&lt;/h2&gt;
&lt;h3 id=&quot;1-deactivating-plugins-instead-of-neutralizing-them&quot;&gt;1. Deactivating plugins instead of neutralizing them&lt;/h3&gt;
&lt;p&gt;The instinct is to deactivate ShopMagic or whatever else looks dangerous. But now you can’t test whether your abandoned cart sequence fires at the right time, whether the discount code generates correctly, whether the email template renders your product data. You miss bugs in automation logic and plugin interactions. You’re testing a system that doesn’t match production.&lt;/p&gt;
&lt;p&gt;The answer isn’t deactivation. It’s neutralization - keep the plugin running, but sever its external connections. The automation still fires, the email still generates - it just lands in MailHog instead of a real inbox.&lt;/p&gt;
&lt;h3 id=&quot;2-relying-on-plugin-test-mode-settings&quot;&gt;2. Relying on plugin “test mode” settings&lt;/h3&gt;
&lt;p&gt;“The plugin has a test mode checkbox. I’ll just enable it after sync.”&lt;/p&gt;
&lt;p&gt;The problem: those settings live in the database you just cloned. Production values overwrite whatever you configured last time. Miss one plugin, and you have a data leak.&lt;/p&gt;
&lt;h3 id=&quot;3-trusting-wp_environment_type&quot;&gt;3. Trusting &lt;code&gt;WP_ENVIRONMENT_TYPE&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;“My plugins detect the environment automatically.”&lt;/p&gt;
&lt;p&gt;Many plugins ignore &lt;code&gt;WP_ENVIRONMENT_TYPE&lt;/code&gt; entirely. Legacy code paths bypass environment checks. Third-party integrations don’t follow WordPress conventions. This constant is a hint, not a guarantee.&lt;/p&gt;
&lt;h3 id=&quot;4-manual-checklists&quot;&gt;4. Manual checklists&lt;/h3&gt;
&lt;p&gt;A 47-point staging setup checklist sounds thorough until someone adds a new plugin and forgets to update it. Or a junior developer inherits the project. Or you’re under pressure on a Friday afternoon and skip three steps.&lt;/p&gt;
&lt;p&gt;Humans are unreliable. Automation isn’t.&lt;/p&gt;
&lt;h3 id=&quot;5-forgetting-background-processes&quot;&gt;5. Forgetting background processes&lt;/h3&gt;
&lt;p&gt;You deactivated the email plugin, but WP-Cron jobs are still queued. Action Scheduler tasks persist in the database. Plugin-specific scheduled tasks fire regardless of what’s active in the admin panel.&lt;/p&gt;
&lt;p&gt;Deactivating a plugin doesn’t clear its queue. The damage is already loaded.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;the-right-approach-neutralize-dont-deactivate&quot;&gt;The Right Approach: Neutralize, Don’t Deactivate&lt;/h2&gt;
&lt;p&gt;The pattern we use across &lt;a href=&quot;https://shift64.com/services/woocommerce-care&quot;&gt;WooCommerce Care&lt;/a&gt; deployments is straightforward:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep every plugin active. Cut every external connection.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This means: replace production API keys with sandbox credentials, route all email through a local catch-all, swap analytics IDs with dummy values, redirect webhooks to null endpoints, and clear every background queue.&lt;/p&gt;
&lt;p&gt;The staging site should behave identically to production in every way - except nothing escapes.&lt;/p&gt;
&lt;h3 id=&quot;the-universal-email-trap&quot;&gt;The Universal Email Trap&lt;/h3&gt;
&lt;p&gt;The simplest, most reliable safety net is an MU-plugin that intercepts every email WordPress sends and redirects it to a single catch-all address. This works regardless of which plugin sends the email - WooCommerce, ShopMagic, membership plugins, custom code, anything that uses &lt;code&gt;wp_mail()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;wp-content/mu-plugins/staging-email-trap.php&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;lt;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Plugin Name: Staging Email Trap&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Description: Redirects all emails to a single catch-all address on non-production environments.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;defined&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;WP_ENVIRONMENT_TYPE&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; WP_ENVIRONMENT_TYPE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;production&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    add_filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;wp_mail&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($args) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $catch_all &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;staging@example.com&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // Preserve original recipient&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $original_to &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; is_array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;to&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; implode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;, &amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;to&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;to&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;subject&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;[STAGING → &amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $original_to &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;] &amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;subject&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // Redirect email&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;to&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $catch_all;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;empty&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;headers&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            $is_string &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; is_string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;headers&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            $headers &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $is_string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; preg_split&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\r\n&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\r&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;headers&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;headers&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            $headers &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; array_filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($headers, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ($header) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                $header &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; trim&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($header);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; stripos&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($header, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;cc:&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; stripos&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($header, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;bcc:&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            $args[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;headers&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $is_string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; implode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\r\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, $headers)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $headers;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $args;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On any non-production environment, it intercepts every &lt;code&gt;wp_mail()&lt;/code&gt; call&lt;/li&gt;
&lt;li&gt;Rewrites the recipient to your catch-all address&lt;/li&gt;
&lt;li&gt;Prepends the original recipient to the subject line so you can see who &lt;em&gt;would have&lt;/em&gt; received it&lt;/li&gt;
&lt;li&gt;Strips CC and BCC headers to prevent any leakage&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Why this is better than plugin-by-plugin neutralization:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One file, covers everything&lt;/li&gt;
&lt;li&gt;Works for plugins you haven’t audited yet&lt;/li&gt;
&lt;li&gt;Works for custom code that sends email directly&lt;/li&gt;
&lt;li&gt;Doesn’t require understanding each plugin’s internals&lt;/li&gt;
&lt;li&gt;The email still generates and sends - you can inspect templates, content, timing - it just lands safely in your inbox instead of a customer’s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What this doesn’t catch.&lt;/strong&gt; The &lt;code&gt;wp_mail&lt;/code&gt; filter only intercepts emails that go through WordPress’s built-in mail function. Three categories of plugins bypass it entirely:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Direct PHPMailer usage.&lt;/strong&gt; Some plugins instantiate PHPMailer directly instead of calling &lt;code&gt;wp_mail()&lt;/code&gt;. In our stack, we disable PHPMailer’s default send method at the server level - which is good practice for other reasons too (deliverability, logging, security). But if your setup allows raw PHPMailer, the trap above won’t see those sends.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External API senders.&lt;/strong&gt; Plugins that send via Mailgun, SendGrid, or Brevo API calls never touch &lt;code&gt;wp_mail()&lt;/code&gt; at all. The email leaves PHP as an HTTP request to a third-party API - invisible to any WordPress mail filter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins with their own SMTP stack.&lt;/strong&gt; MailPoet’s Bridge service, some transactional email plugins, and certain SMTP plugins maintain their own sending connection. They bypass both &lt;code&gt;wp_mail()&lt;/code&gt; and PHPMailer.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; audit your plugin stack. Run a test email from every plugin that sends mail and check whether it lands in your catch-all. If it doesn’t - that plugin is bypassing &lt;code&gt;wp_mail()&lt;/code&gt; and needs its own neutralization (API key removal, endpoint blocking, or the plugin-specific MU-plugin approach we show in the MailPoet case study below).&lt;/p&gt;
&lt;h3 id=&quot;the-post-sync-sequence&quot;&gt;The Post-Sync Sequence&lt;/h3&gt;
&lt;p&gt;After importing the production database to staging, run this sequence &lt;strong&gt;before any WordPress bootstrapping&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 1. Temporarily deactivate plugins that auto-execute on load&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; deactivate&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; mailpoet&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; woocommerce-gateway-stripe&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --quiet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 2. Run neutralization (swap keys, sanitize data, clear queues)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; eval-file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; neutralize-staging.php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 3. Reactivate with safe configuration&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; activate&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; mailpoet&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; woocommerce-gateway-stripe&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --quiet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 4. Verify&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; cron&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; event&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; list&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --format=table&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; eval&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;wp_mail(&amp;#39;test@staging.local&amp;#39;, &amp;#39;Test&amp;#39;, &amp;#39;Body&amp;#39;);&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ↑ This should land in MailHog, not real SMTP&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The deactivate-neutralize-reactivate sequence is critical. Some plugins fire hooks on activation that would use the production credentials still in the database. You need the credentials swapped before the plugin loads.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;case-study-neutralizing-mailpoet&quot;&gt;Case Study: Neutralizing MailPoet&lt;/h2&gt;
&lt;p&gt;MailPoet is a good example of why staging neutralization matters. It stores sending queues and subscriber data in custom database tables, can take over WooCommerce transactional emails, and connects to an external sending service (MailPoet Bridge) for the paid version.&lt;/p&gt;
&lt;p&gt;The question with any email plugin on staging is: will it actually send? The paid MailPoet sending service may reject sends from a mismatched staging domain - but “may” isn’t good enough when 50,000 real subscriber emails are in the database. The safe approach is to neutralize regardless, so you never have to wonder.&lt;/p&gt;
&lt;h3 id=&quot;step-1-kill-the-sending-pipeline&quot;&gt;Step 1: Kill the sending pipeline&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- Force MailPoet to use PHP mail() (which we route to MailHog)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wp_mailpoet_settings&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;website&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;mta_group&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- Remove API keys that authenticate with MailPoet&amp;#39;s bridge&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;DELETE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wp_mailpoet_settings&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; IN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;premium_key&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;mss_key&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-2-clear-every-queue&quot;&gt;Step 2: Clear every queue&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;DELETE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wp_mailpoet_scheduled_tasks&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; status&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; IN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;scheduled&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;paused&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TRUNCATE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wp_mailpoet_sending_queues;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-3-sanitize-subscriber-data&quot;&gt;Step 3: Sanitize subscriber data&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wp_mailpoet_subscribers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; email &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; CONCAT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;user_&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, id, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;@mailhog.staging.local&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; email &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;NOT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LIKE&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;%@yourcompany.com&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even if something slips past steps 1 and 2, the emails go to fake addresses that route to your local catch-all.&lt;/p&gt;
&lt;h3 id=&quot;defense-in-depth-the-mu-plugin-safety-net&quot;&gt;Defense in depth: the MU-plugin safety net&lt;/h3&gt;
&lt;p&gt;SQL neutralization handles the database. But for an extra layer of protection, deploy a must-use plugin that blocks MailPoet’s external connections at the PHP level:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;wp-content/mu-plugins/staging-safety-mailpoet.php&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;lt;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;Plugin Name: MailPoet Staging Safety&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;Description: Neutralizes MailPoet on non-production environments&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;*/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;defined&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;WP_ENVIRONMENT_TYPE&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; WP_ENVIRONMENT_TYPE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;production&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Kill MailPoet&amp;#39;s independent cron system&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    add_filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;mailpoet_cron_enabled&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;__return_false&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Inject error into the sending pipeline&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    add_filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;mailpoet_sending_methods_errors&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($errors) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $errors[] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;STAGING SAFETY: Sending blocked by mu-plugin&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $errors;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Block all requests to MailPoet&amp;#39;s bridge service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    add_filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;pre_http_request&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($response, $args, $url) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;strpos&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($url, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;bridge.mailpoet.com&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; WP_Error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;staging_block&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;Blocked on staging&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $response;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This MU-plugin loads before MailPoet does. Even if someone restores the production database and forgets to run the neutralization script, the safety net catches it.&lt;/p&gt;
&lt;p&gt;That’s three independent layers: database neutralization, email sanitization, and a PHP-level block. All three would have to fail simultaneously for a real email to escape.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;the-neutralization-matrix&quot;&gt;The Neutralization Matrix&lt;/h2&gt;
&lt;p&gt;Every plugin with external connections needs the same analysis. Here’s the reference:&lt;/p&gt;

































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Plugin&lt;/th&gt;&lt;th&gt;Risk&lt;/th&gt;&lt;th&gt;Neutralization&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Stripe&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Real charges&lt;/td&gt;&lt;td&gt;Swap to test API keys (well-documented test mode)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PayPal&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Real charges&lt;/td&gt;&lt;td&gt;Sandbox credentials&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GA4 / GTM&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Analytics pollution&lt;/td&gt;&lt;td&gt;Dummy measurement ID&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Meta Pixel&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;False conversion events&lt;/td&gt;&lt;td&gt;Test pixel ID or block outbound requests&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ShopMagic&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Abandoned cart emails to real customers&lt;/td&gt;&lt;td&gt;Dequeue scheduled automations + route email to catch-all&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;WooCommerce Subscriptions&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Recurring charges on real cards&lt;/td&gt;&lt;td&gt;Test gateway + test mode flag&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MailPoet&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Mass email, sender reputation damage&lt;/td&gt;&lt;td&gt;Switch to PHP mail() + sanitize subscribers + clear queues&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Klaviyo / Mailchimp&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;List pollution, real email sends&lt;/td&gt;&lt;td&gt;Disconnect API or swap to test account&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Membership plugins&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Renewal emails to real members&lt;/td&gt;&lt;td&gt;Route email to catch-all + clear scheduled renewals&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Zapier / Webhooks&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Triggers production automations&lt;/td&gt;&lt;td&gt;Null endpoints or staging-specific URLs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Action Scheduler&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Fires queued tasks from production&lt;/td&gt;&lt;td&gt;Purge pending/scheduled actions&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;For each plugin: identify where credentials live (wp_options, custom tables, wp-config constants), write the neutralization SQL, automate it with WP-CLI, and add a safety-net MU-plugin if the risk justifies it.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;automate-or-accept-the-risk&quot;&gt;Automate or Accept the Risk&lt;/h2&gt;
&lt;p&gt;The neutralization steps above aren’t complicated. The problem is consistency.&lt;/p&gt;
&lt;p&gt;On your tenth staging sync, at 6 PM on a Friday, with a client waiting for a fix - that’s when steps get skipped. That’s when someone forgets that the new Klaviyo integration wasn’t in the original checklist.&lt;/p&gt;
&lt;p&gt;The answer is a post-sync script that runs automatically:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;#!/bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# post-sync.sh - runs immediately after database import&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -e&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;=== Staging neutralization ===&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Deactivate plugins that auto-execute&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; deactivate&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; mailpoet&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; woocommerce-gateway-stripe&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --quiet&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; 2&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/dev/null&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Run neutralization&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; eval-file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; neutralize-staging.php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Reactivate with safe config&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; activate&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; mailpoet&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; woocommerce-gateway-stripe&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --quiet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Clear all caches&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; cache&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; flush&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; rewrite&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; flush&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;=== Staging secured ===&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Commit this script to the repository. Run it as part of every sync. No exceptions.&lt;/p&gt;
&lt;p&gt;Over time, you build a neutralization library - one script per risky plugin. New project? Apply the relevant scripts. New plugin added to production? Audit it, write the neutralizer, add it to the pipeline.&lt;/p&gt;
&lt;p&gt;Once the staging environment is neutralized, the next step is verifying that everything still works. &lt;a href=&quot;https://shift64.com/services/playwright-testing&quot;&gt;Automated Playwright tests&lt;/a&gt; can walk the full purchase flow - add to cart, checkout, payment, order confirmation - on the neutralized staging clone before anything touches production.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;the-paradox-resolved&quot;&gt;The Paradox, Resolved&lt;/h2&gt;
&lt;p&gt;The ideal staging environment is not a perfect copy of production. It’s a &lt;strong&gt;functionally identical but externally dead&lt;/strong&gt; mirror.&lt;/p&gt;
&lt;p&gt;You’re not testing whether ShopMagic can send abandoned cart emails - it can. You’re testing whether your automation triggers at the right time, whether the discount code generates correctly, whether the email template renders your product data.&lt;/p&gt;
&lt;p&gt;The email itself lands in MailHog. You inspect it. No consequences.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Test the same code. Capture the same events. Let nothing escape.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s not a compromise. It’s the only way staging actually works.&lt;/p&gt;
&lt;p&gt;This process - staging neutralization, automated verification, safe deployment - is exactly what we deliver as part of &lt;a href=&quot;https://shift64.com/services/woocommerce-care&quot;&gt;WooCommerce Care&lt;/a&gt;. Every update goes through a sterile staging environment before it touches production. If you’re tired of crossing your fingers after every deploy, &lt;a href=&quot;https://shift64.com/#start&quot;&gt;let’s talk&lt;/a&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;staging-safety-checklist&quot;&gt;Staging Safety Checklist&lt;/h2&gt;
&lt;p&gt;Before declaring your staging environment ready:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All email routed to a local catch-all (MailHog, Mailtrap)&lt;/li&gt;
&lt;li&gt;Payment gateways using sandbox/test API keys&lt;/li&gt;
&lt;li&gt;Analytics IDs replaced with dummy values&lt;/li&gt;
&lt;li&gt;Marketing pixels disabled or using test accounts&lt;/li&gt;
&lt;li&gt;Webhook endpoints redirected to staging or null URLs&lt;/li&gt;
&lt;li&gt;Background queues cleared (WP-Cron, Action Scheduler, plugin-specific)&lt;/li&gt;
&lt;li&gt;All production API keys replaced with sandbox equivalents&lt;/li&gt;
&lt;li&gt;Customer and subscriber emails sanitized&lt;/li&gt;
&lt;li&gt;License keys removed or replaced with dev licenses&lt;/li&gt;
&lt;li&gt;MU-plugin safety net deployed&lt;/li&gt;
&lt;li&gt;Post-sync script tested, committed, and running automatically&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>woocommerce</category><category>staging</category><category>devops</category><category>wordpress</category><category>security</category></item><item><title>Why Hosting Should Be Step One in WordPress Optimization</title><link>https://shift64.com/blog/why-hosting-should-be-step-one-in-wordpress-optimization</link><guid isPermaLink="true">https://shift64.com/blog/why-hosting-should-be-step-one-in-wordpress-optimization</guid><description>Code optimization on a terrible host is a dead-end loop. I share real benchmarks - same code, two servers - and why the order of operations fundamentally changes the outcome.</description><pubDate>Sun, 08 Feb 2026 16:51:25 GMT</pubDate><content:encoded>&lt;p&gt;There’s a recurring pattern in the WordPress world: a client reports that their site is slow. The specialist dives into profiling, analyzing database queries, trimming plugins. Hosting? Left untouched. After all, you should optimize what you have before throwing money at hardware.&lt;/p&gt;
&lt;nav class=&quot;toc-component not-prose my-10 border-4 border-black bg-white p-6 shadow-[6px_6px_0px_0px_#000000]&quot;&gt; &lt;h2 class=&quot;mb-4 text-xl font-black font-heading uppercase tracking-tight&quot;&gt; Table of Contents &lt;/h2&gt; &lt;ol class=&quot;toc-list space-y-2&quot;&gt;&lt;/ol&gt; &lt;/nav&gt; &lt;script type=&quot;module&quot; src=&quot;https://shift64.com/opt/buildhome/repo/src/components/astro/TableOfContents.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;Sounds reasonable - and in many cases it is. But there’s a line beyond which this approach becomes &lt;strong&gt;fundamentally wrong&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That line is where a server returns multi-second TTFB on a simple business site - Gutenberg, 7 plugins, no WooCommerce, no Elementor - without any caching.&lt;/p&gt;
&lt;p&gt;A site like this on a decent host should achieve a TTFB of 200-300 milliseconds with zero caching. Google considers 800ms the acceptable threshold. I wouldn’t even raise an eyebrow if TTFB hovered around 800-1200ms - for many, that’s still an acceptable trade-off relative to cost.&lt;/p&gt;
&lt;p&gt;But 3-4 seconds? That’s &lt;strong&gt;several hundred percent&lt;/strong&gt; above the norm. And no amount of code optimization will fix it.&lt;/p&gt;
&lt;h2 id=&quot;a-ticking-time-bomb&quot;&gt;A Ticking Time Bomb&lt;/h2&gt;
&lt;p&gt;Let’s say you manage to squeeze decent results out of such a host using cache. The site loads, the client is happy.&lt;/p&gt;
&lt;p&gt;But you’ll never reach a 100% cache hit ratio.&lt;/p&gt;
&lt;p&gt;And once the optimization process wraps up and ongoing oversight disappears, it only takes one of the following to bring the whole thing crashing down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new traffic channel with URL parameters that the cache doesn’t ignore&lt;/li&gt;
&lt;li&gt;A new plugin or plugin update that conflicts with the cache configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few months later, the site owner might launch an expensive ad campaign, and instead of getting cached pages, visitors wait 4 seconds per load.&lt;/p&gt;
&lt;p&gt;The campaign has an absurd bounce rate because of one small oversight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The entire optimization investment - wasted.&lt;/strong&gt; Because underneath it all, there’s still a terrible server.&lt;/p&gt;
&lt;h2 id=&quot;my-data---same-code-two-servers&quot;&gt;My Data - Same Code, Two Servers&lt;/h2&gt;
&lt;p&gt;I recently took on the optimization of a WordPress site. It was sitting on a weak host, so from the very beginning I made it clear: &lt;strong&gt;there’s no point investing in code optimization if we leave the site on this server&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We agreed on a migration to proper hosting, and in the meantime I handled the optimization on a dev environment. Once it was done, I ran benchmarks on both environments - to get hard data.&lt;/p&gt;
&lt;h3 id=&quot;results-on-the-old-host&quot;&gt;Results on the Old Host&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Old theme (before optimization):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Times (TTFB, no cache): 2.16s, 2.55s, 2.52s, 2.52s, 3.47s, 4.14s, 4.24s, 2.93s, 2.90s, 2.61s, 2.89s, 2.36s, 2.44s&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Average: 2.90s&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New theme (after optimization):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Times: 2.29s, 2.15s, 2.85s, 3.45s, 2.52s, 2.28s, 2.64s, 3.72s, 2.43s, 2.33s, 2.47s, 2.13s&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Average: 2.61s&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;10% faster. Still &lt;strong&gt;326% above&lt;/strong&gt; the recommended TTFB threshold.&lt;/p&gt;
&lt;p&gt;Query Monitor showed nearly a full second faster than the &lt;code&gt;cfOrigin&lt;/code&gt; header from Cloudflare. In Query Monitor, the difference in database query times was clear - the optimization was clearly working. But the LiteSpeed server added so much overhead that the whole effort translated to a &lt;strong&gt;modest 10% improvement with still terrible results&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Be honest - would you pay a developer for a day and a half of work for that kind of progress? I wouldn’t.&lt;/p&gt;
&lt;h3 id=&quot;results-on-the-target-host&quot;&gt;Results on the Target Host&lt;/h3&gt;
&lt;p&gt;Same theme versions, same code - but on a proper server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Old theme on the new host:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/hosting-as-first/old-theme-benchmarks.jpg&quot; alt=&quot;Performance benchmarks for the old theme on the new host - average TTFB 0.49s&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New theme on the new host:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/hosting-as-first/optimized-theme-benchmarks.jpg&quot; alt=&quot;Performance benchmarks for the optimized theme on the new host - average TTFB 0.38s&quot;/&gt;&lt;/p&gt;
&lt;p&gt;In addition to full TTFB, I also measured the server’s wait time - how long the server actually spent processing the request: PHP + Nginx.&lt;/p&gt;
&lt;p&gt;Full TTFB: &lt;strong&gt;0.38s vs 0.49s&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Server wait time: &lt;strong&gt;0.21s vs 0.33s&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;22% improvement&lt;/strong&gt; and TTFB with &lt;strong&gt;over 400ms of headroom&lt;/strong&gt; below the recommended threshold.&lt;/p&gt;
&lt;p&gt;If we strip out connection time and focus on the PHP + Nginx processing alone, the server delivers the page &lt;strong&gt;36% faster&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That looks a bit better than 10% and times above 2.5s.&lt;/p&gt;
&lt;h2 id=&quot;foundation-first-optimization-second&quot;&gt;Foundation First, Optimization Second&lt;/h2&gt;
&lt;p&gt;Only after confirming real improvements do I move on to configuring cache.&lt;/p&gt;
&lt;p&gt;Because even if I can’t predict every scenario - it’ll still be fine.&lt;/p&gt;
&lt;p&gt;I also know that even a logged-in user (who won’t get a cached page) will be able to use the site quickly and comfortably.&lt;/p&gt;
&lt;h3 id=&quot;back-to-the-numbers&quot;&gt;Back to the Numbers&lt;/h3&gt;
&lt;p&gt;Let’s look at this from a business perspective.&lt;/p&gt;
&lt;p&gt;If the old server could routinely add &lt;strong&gt;a full second&lt;/strong&gt; of overhead (measured as the difference between Query Monitor timing and &lt;code&gt;cfOrigin&lt;/code&gt;), then even investing hundreds of thousands and rewriting the application from scratch, we wouldn’t come close to matching the old theme version served from a better host.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You’re always limited by that one second of terrible web server overhead.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Sometimes it’ll be better, sometimes worse - but by trying to save on proper infrastructure, you can end up in an endless loop of development work that never reaches a happy ending.&lt;/p&gt;
&lt;p&gt;And if you flip the order?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Migration alone&lt;/strong&gt; improves performance from &lt;strong&gt;2.90s to 0.49s&lt;/strong&gt;. That’s nearly &lt;strong&gt;6x faster&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Zero code analysis.&lt;/p&gt;
&lt;p&gt;Zero refactoring.&lt;/p&gt;
&lt;p&gt;One quick migration to a good host.&lt;/p&gt;
&lt;p&gt;One day - and you have your result.&lt;/p&gt;
&lt;p&gt;The costs may look higher if you compare hardware price lists side by side.&lt;/p&gt;
&lt;p&gt;But if you factor in developer time, the migration alone might turn out to be far cheaper.&lt;/p&gt;
&lt;h2 id=&quot;stop-tolerating-bad-hosting&quot;&gt;Stop Tolerating Bad Hosting&lt;/h2&gt;
&lt;p&gt;Years of price wars and dirty tricks in the hosting market have led to a situation where &lt;strong&gt;people accept as fact&lt;/strong&gt; that hosting can be cheap and slow. And they just live with it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That’s just how WordPress is!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We look the other way and try to mask it with cache. And that’s a straight path to making WordPress look like a joke as a platform.&lt;/p&gt;
&lt;p&gt;Look at it from the perspective of a business considering WordPress for a new project.&lt;/p&gt;
&lt;p&gt;Imagine a decision-maker stumbles upon an optimization course and sees that to make a &lt;strong&gt;simple business website&lt;/strong&gt; reasonably fast on WordPress, they need to hire a performance specialist and dedicate three months to the process.&lt;/p&gt;
&lt;p&gt;What do you think they’ll do?&lt;/p&gt;
&lt;p&gt;Choose WordPress, or go with Astro, Next.js, or any other technology where this problem simply doesn’t exist?&lt;/p&gt;
&lt;p&gt;By accepting this status quo, &lt;strong&gt;we as WordPress developers are shooting ourselves in the foot&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We normalize performance pathology and teach clients that “this is just how it works.” And then we wonder why WordPress is losing market share.&lt;/p&gt;
&lt;p&gt;Of course, we should always strive for good design patterns and solve problems at the design stage - not patch them later when the site is already tanking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But if the starting point of our project exceeds acceptable norms by hundreds of percent due to bad infrastructure, it’s our responsibility to draw a clear line and say: stop.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s stop tolerating this.&lt;/p&gt;
&lt;p&gt;This is exactly the thinking behind &lt;a href=&quot;https://shift64.com/black-box-deception&quot;&gt;SHIFT64&lt;/a&gt; - because I believe solid infrastructure should be a foundation, not a luxury.&lt;/p&gt;</content:encoded><category>wordpress</category><category>optimization</category><category>hosting</category><category>performance</category><category>ttfb</category></item><item><title>Shift64 - Built with Astro</title><link>https://shift64.com/blog/shift64-built-with-astro</link><guid isPermaLink="true">https://shift64.com/blog/shift64-built-with-astro</guid><description>A case study on building the Shift64 website using Astro, exploring the technology choices, architecture decisions, and lessons learned.</description><pubDate>Thu, 29 Jan 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;SHIFT64 is a digital agency focused on high-performance WooCommerce infrastructure. When it came time to build our own website, we needed a framework that matched our philosophy: speed, flexibility, and no unnecessary bloat. Astro was the natural choice.&lt;/p&gt;
&lt;nav class=&quot;toc-component not-prose my-10 border-4 border-black bg-white p-6 shadow-[6px_6px_0px_0px_#000000]&quot;&gt; &lt;h2 class=&quot;mb-4 text-xl font-black font-heading uppercase tracking-tight&quot;&gt; Table of Contents &lt;/h2&gt; &lt;ol class=&quot;toc-list space-y-2&quot;&gt;&lt;/ol&gt; &lt;/nav&gt; &lt;script type=&quot;module&quot; src=&quot;https://shift64.com/opt/buildhome/repo/src/components/astro/TableOfContents.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/shift64-astro/homepage_1.jpeg&quot; alt=&quot;SHIFT64 Homepage Overview&quot;/&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-astro&quot;&gt;Why Astro?&lt;/h2&gt;
&lt;p&gt;Astro ships zero JavaScript by default. For a content-heavy site like ours, this translates directly into faster page loads and better Core Web Vitals. But performance was only part of the equation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Island Architecture&lt;/strong&gt; lets us sprinkle interactivity exactly where we need it. A static pricing page stays static. A consent banner or contact form hydrates independently. No monolithic JavaScript bundle forcing users to download code they will never execute.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Collections&lt;/strong&gt; give us type-safe content management without a heavy CMS. We define schemas, and Astro validates our data at build time. Broken links, missing fields, or malformed dates get caught before deployment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shuffle Templates + Tailwind CSS&lt;/strong&gt; gave us a head start on design. We used &lt;a href=&quot;https://shuffle.dev/&quot;&gt;Shuffle&lt;/a&gt; neubrutalism templates as a foundation—bold outlines, flat colors, strong shadows—and customized them with Tailwind to create a distinctive visual identity. The result is a look that stands out in a sea of generic agency websites while maintaining full design control through utility classes.&lt;/p&gt;
&lt;h2 id=&quot;what-we-built&quot;&gt;What We Built&lt;/h2&gt;
&lt;p&gt;This case study walks through three custom features we implemented:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lessons Module&lt;/strong&gt; — A protected content delivery system with token-based access and direct Brevo integration for efficient marketing consent updates.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Custom Offers CMS&lt;/strong&gt; — An internal tool built on Cloudflare D1 for sending instant, custom audit proposals to clients.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Consent &amp;amp; Analytics Module&lt;/strong&gt; — A privacy-first approach that loads services dynamically based on environment configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each section covers the architecture decisions, the tradeoffs we made, and practical takeaways you can apply to your own projects.&lt;/p&gt;
&lt;h2 id=&quot;lessons-module-protected-content-delivery&quot;&gt;Lessons Module: Protected Content Delivery&lt;/h2&gt;
&lt;p&gt;Our &lt;a href=&quot;https://shift64.com/course&quot;&gt;WooCommerce Performance Course&lt;/a&gt; needed a way to deliver exclusive training materials to registered users. Rather than bolting on a heavy LMS plugin, we built a lightweight system using Astro’s content collections and middleware.&lt;/p&gt;
&lt;h3 id=&quot;course-structure&quot;&gt;Course Structure&lt;/h3&gt;
&lt;p&gt;The content follows a simple hierarchy: a course contains lessons, and each lesson contains the actual learning material. Lessons are stored as MDX files, which means we can mix Markdown prose with interactive components like audio players.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/shift64-astro/course_2.jpeg&quot; alt=&quot;Course Overview with Lesson Cards&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Each lesson defines its metadata in frontmatter—title, lesson number, description, and whether it is published. Astro validates this schema at build time, catching any missing fields before deployment. Unpublished lessons appear in the navigation with a “#SOON” badge, giving users visibility into what is coming next.&lt;/p&gt;
&lt;h3 id=&quot;token-based-access&quot;&gt;Token-Based Access&lt;/h3&gt;
&lt;p&gt;We implemented a passwordless authentication flow. Users sign up with their email, and when they want to access content, they request a magic link. That link contains a short-lived token (15 minutes) that, once verified, creates a longer session (30 days).&lt;/p&gt;
&lt;p&gt;The experience is frictionless: no passwords to remember, no account management overhead. Users click a link in their inbox and they are in. The session token lives in an httpOnly cookie, keeping it secure from client-side scripts.&lt;/p&gt;
&lt;h3 id=&quot;backend-validation-with-brevo&quot;&gt;Backend Validation with Brevo&lt;/h3&gt;
&lt;p&gt;When a user requests access, we don’t just generate a token blindly. The API route connects directly to &lt;strong&gt;Brevo’s API&lt;/strong&gt; to verify the contact’s status. It checks if the email exists in our specific “Course Participants” list and verifies they have given marketing consent.&lt;/p&gt;
&lt;p&gt;Only if these checks pass does the system generate the magic link. This ensures that only authorized users who have opted-in can access the educational content, keeping our email capability compliant and our content secure.&lt;/p&gt;
&lt;h3 id=&quot;middleware-protection&quot;&gt;Middleware Protection&lt;/h3&gt;
&lt;p&gt;Astro’s middleware runs on every request before the page renders. Our middleware extracts the session cookie, verifies the token, and sets an &lt;code&gt;isAuthenticated&lt;/code&gt; flag that pages and components can read.&lt;/p&gt;
&lt;p&gt;Protected content uses a Paywall component. When authenticated, users see the full lesson. When not, they see a preview with a gradient fade and a prompt to log in. The protection happens server-side—there is no hidden content in the HTML for unauthenticated users to inspect.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/shift64-astro/course-lesson_1.jpeg&quot; alt=&quot;Lesson Page with Audio Player and Navigation&quot;/&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-learning-experience&quot;&gt;The Learning Experience&lt;/h3&gt;
&lt;p&gt;Navigation between lessons is straightforward. Each lesson page includes previous and next buttons that respect the lesson numbering. Users can also jump back to the course overview at any time to see all available lessons.&lt;/p&gt;
&lt;p&gt;We kept the experience intentionally simple. No progress bars, no completion certificates, no gamification. The focus stays on the content itself. Analytics tracking happens in the background for our business intelligence, but it does not affect the user interface.&lt;/p&gt;
&lt;h2 id=&quot;custom-offers-cms-cloudflare-d1--fast-edits&quot;&gt;Custom Offers CMS: Cloudflare D1 &amp;amp; Fast Edits&lt;/h2&gt;
&lt;p&gt;Originally, we considered a file-based approach for our pricing and offers, but we needed something more robust for custom client proposals. We built an internal CMS that allows us to generate unique, uuid-slugged offer pages (e.g., &lt;a href=&quot;https://shift64.com/audit/94b5fcc2-de2a-467b-bebb-ac1758b490fe&quot;&gt;see a sample audit&lt;/a&gt;) instantly.&lt;/p&gt;
&lt;h3 id=&quot;powered-by-cloudflare-d1&quot;&gt;Powered by Cloudflare D1&lt;/h3&gt;
&lt;p&gt;We chose &lt;strong&gt;Cloudflare D1&lt;/strong&gt;—a serverless SQL database—to store our offer data. This gives us incredible read performance at the edge, ensuring that clients see their audits instantly, no matter where they are located.&lt;/p&gt;
&lt;p&gt;The CMS is simple but powerful. We can create a new audit offer, set the title, embed a video (like a Loom or Descript walkthrough), and write the micro audit in &lt;strong&gt;Markdown&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/shift64-astro/admin-area_1.jpeg&quot; alt=&quot;Internal CMS Audit Editor&quot;/&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-workflow&quot;&gt;The Workflow&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create Offer&lt;/strong&gt;: We generate a new entry in the Admin dashboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write Content&lt;/strong&gt;: We draft the specific findings and recommendations in Markdown.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embed Media&lt;/strong&gt;: We drop in a video embed code for a personal walkthrough.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publish&lt;/strong&gt;: One click pushes it live to the edge.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because the data lives in D1 and the frontend is Astro, the pages are dynamic but feel as fast as static content. There’s no build step required to update an offer—changes are live the moment we hit save.&lt;/p&gt;
&lt;h2 id=&quot;consent--analytics-module-privacy-first-tracking&quot;&gt;Consent &amp;amp; Analytics Module: Privacy-First Tracking&lt;/h2&gt;
&lt;p&gt;GDPR compliance is not optional, but that does not mean analytics has to be an afterthought. We built a consent system that respects user choices while maintaining the insights we need to improve the site. The guiding principle: the site works perfectly without any consent given.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://shift64.com/blog/shift64-astro/consent_1.jpeg&quot; alt=&quot;Consent Banner on First Visit&quot;/&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-consent-banner&quot;&gt;The Consent Banner&lt;/h3&gt;
&lt;p&gt;When visitors first arrive, they see a consent banner with three clear options: Accept All, Reject All, or Customize. No dark patterns, no pre-checked boxes, no confusing language. The banner explains what we track and why.&lt;/p&gt;
&lt;p&gt;Choosing “Customize” reveals individual toggles for each tracking service. Users can enable analytics while blocking marketing pixels, or vice versa. Their choice persists for 180 days across all subdomains.&lt;/p&gt;
&lt;h3 id=&quot;consent-categories&quot;&gt;Consent Categories&lt;/h3&gt;
&lt;p&gt;We track four services, each serving a distinct purpose:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Google Analytics (GA4)&lt;/strong&gt; — Traffic patterns, page performance, and user journeys. Helps us understand which content resonates.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Microsoft Clarity&lt;/strong&gt; — Heatmaps and session recordings. Shows us where users click, scroll, and get stuck.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Meta Pixel&lt;/strong&gt; — Conversion tracking for advertising campaigns. Only relevant when running paid social.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Analytics&lt;/strong&gt; — Performance metrics at the edge. Lightweight and privacy-focused.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;environment-based-loading&quot;&gt;Environment-Based Loading&lt;/h3&gt;
&lt;p&gt;To keep our system flexible, we architected the &lt;code&gt;ConsentManager&lt;/code&gt; to check for the existence of specific &lt;strong&gt;environment variables&lt;/strong&gt; before even offering a service.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;META_PIXEL_ID&lt;/code&gt; is present in the &lt;code&gt;.env&lt;/code&gt; (or Cloudflare secrets), the Meta Pixel service is loaded into the consent engine. If we remove that variable, the service completely disappears from the frontend—no code delivered, no options shown to the user. This allows us to turn tracking tools on and off infrastructure-wide without deploying code changes.&lt;/p&gt;
&lt;h3 id=&quot;graceful-degradation&quot;&gt;Graceful Degradation&lt;/h3&gt;
&lt;p&gt;Here is the key architectural decision: every tracking service defaults to “denied” until explicit consent. This is not just a legal checkbox—it is how the system actually works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google Analytics&lt;/strong&gt; uses Consent Mode v2. Before the gtag script even loads, we set all storage permissions to denied. No cookies get written, no data collected. When users grant consent, we update the consent state and GA begins tracking. Revoke consent later? The state updates again and tracking stops.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microsoft Clarity&lt;/strong&gt; follows the same pattern with its own Consent Mode v2 implementation. The script loads, but waits for our signal before recording anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Meta Pixel&lt;/strong&gt; checks consent before initializing. If denied, &lt;code&gt;fbq(&amp;#39;consent&amp;#39;, &amp;#39;revoke&amp;#39;)&lt;/code&gt; ensures the pixel stays dormant. Granting consent triggers &lt;code&gt;fbq(&amp;#39;consent&amp;#39;, &amp;#39;grant&amp;#39;)&lt;/code&gt; and tracking begins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Analytics&lt;/strong&gt; is the one service that loads automatically on Cloudflare Pages. For users who decline, we run a cookie blocker that monitors and deletes any &lt;code&gt;__cfz_zaraz&lt;/code&gt; cookies every 500 milliseconds. It is aggressive, but it ensures compliance.&lt;/p&gt;
&lt;h3 id=&quot;the-user-experience&quot;&gt;The User Experience&lt;/h3&gt;
&lt;p&gt;The consent flow prioritizes clarity over conversion. We want informed consent, not grudging acceptance.&lt;/p&gt;
&lt;p&gt;First visit shows the banner. Users make a choice. The banner disappears with a smooth animation. Done. No nagging popups, no repeated asks, no banner creeping back after a few pages.&lt;/p&gt;
&lt;p&gt;Need to change your preferences later? A link in the footer reopens the consent panel. Users can update their choices anytime without hunting through settings menus.&lt;/p&gt;
&lt;p&gt;The site itself works identically regardless of consent status. No degraded experience, no “please enable cookies” warnings, no features locked behind consent walls. Analytics are for us, not a prerequisite for the user.&lt;/p&gt;
&lt;h3 id=&quot;cross-tab-synchronization&quot;&gt;Cross-Tab Synchronization&lt;/h3&gt;
&lt;p&gt;One subtle feature: consent syncs across browser tabs. Grant consent in one tab, and other open tabs update immediately via storage events. The consent state also shares across subdomains through cookie domain settings. Log in on &lt;code&gt;www.shift64.com&lt;/code&gt;, and your preference applies to &lt;code&gt;course.shift64.com&lt;/code&gt; too.&lt;/p&gt;
&lt;p&gt;This dual storage approach—localStorage for speed, cookies for cross-domain sharing—handles edge cases that simpler implementations miss.&lt;/p&gt;
&lt;h2 id=&quot;the-blog-youre-reading&quot;&gt;The Blog You’re Reading&lt;/h2&gt;
&lt;p&gt;The blog itself uses Astro’s content collections with MDX for flexible authoring—mixing Markdown with interactive components. Code blocks get Shiki syntax highlighting at build time and a copy-to-clipboard button that appears on hover, making it easy to grab snippets directly into your editor.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Building SHIFT64’s website reinforced what we already believed: the best tools are the ones that get out of your way. Astro gave us the performance baseline. The rest—protected content delivery, type-safe pricing configurations, privacy-first analytics—we built ourselves.&lt;/p&gt;
&lt;p&gt;That is the approach we bring to every project. We do not just configure off-the-shelf solutions and call it done. We dig into requirements, understand tradeoffs, and build systems that actually fit the problem. Whether that means shipping zero JavaScript for a marketing page or engineering a token-based authentication flow, we choose the right tool for the job.&lt;/p&gt;
&lt;p&gt;The result is infrastructure you can trust. Fast, maintainable, and designed around real user needs rather than framework defaults.&lt;/p&gt;
&lt;p&gt;If you are running a WooCommerce store and thinking about performance, hosting, or long-term care, we should talk. Our &lt;a href=&quot;https://shift64.com/black-box-deception&quot;&gt;Black Box Deception&lt;/a&gt; page breaks down exactly what we offer and why transparent infrastructure matters. No pressure—just the information you need to make an informed decision.&lt;/p&gt;</content:encoded><category>Astro</category><category>Case Study</category><category>Web Development</category></item><item><title>Automatic WordPress Theme Versioning with Semantic Release</title><link>https://shift64.com/blog/automatic-wordpress-theme-versioning-semantic-release</link><guid isPermaLink="true">https://shift64.com/blog/automatic-wordpress-theme-versioning-semantic-release</guid><description>Automate WordPress theme versioning end-to-end — from release pipeline setup to daily commits and PRs. One Claude Code skill sets up Semantic Release, three commands handle the rest.</description><pubDate>Sun, 25 Jan 2026 11:36:38 GMT</pubDate><content:encoded>&lt;p&gt;Anyone who works with WordPress themes knows this pain: manually editing versions in &lt;code&gt;style.css&lt;/code&gt;, writing changelogs, remembering all the files when merging. What if I told you that an AI assistant can wire the entire pipeline and then handle your daily commits with short commands?&lt;/p&gt;
&lt;nav class=&quot;toc-component not-prose my-10 border-4 border-black bg-white p-6 shadow-[6px_6px_0px_0px_#000000]&quot;&gt; &lt;h2 class=&quot;mb-4 text-xl font-black font-heading uppercase tracking-tight&quot;&gt; Table of Contents &lt;/h2&gt; &lt;ol class=&quot;toc-list space-y-2&quot;&gt;&lt;/ol&gt; &lt;/nav&gt; &lt;script type=&quot;module&quot; src=&quot;https://shift64.com/opt/buildhome/repo/src/components/astro/TableOfContents.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;the-problem-with-wordpress-versioning&quot;&gt;The Problem with WordPress Versioning&lt;/h2&gt;
&lt;p&gt;WordPress stores the theme version in a CSS comment in &lt;code&gt;style.css&lt;/code&gt;, not in &lt;code&gt;package.json&lt;/code&gt; like modern projects. Standard versioning tools don’t work out-of-the-box.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The bridge between WordPress and Semantic Release is a single &lt;code&gt;sed&lt;/code&gt; command that rewrites the &lt;code&gt;Version:&lt;/code&gt; line in &lt;code&gt;style.css&lt;/code&gt; during each release. Everything else — commit analysis, changelog generation, GitHub releases — works the standard way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The solution is &lt;strong&gt;Semantic Release&lt;/strong&gt; with the right configuration — deployed through a &lt;strong&gt;Claude Code skill&lt;/strong&gt; that handles all the wiring.&lt;/p&gt;
&lt;h2 id=&quot;one-time-setup-activate-the-skill&quot;&gt;One-Time Setup: Activate the Skill&lt;/h2&gt;
&lt;p&gt;Instead of manually creating configs, workflows, and scripts, run one Claude Code command:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/wordpress-theme-semantic-github-deployment&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The skill auto-detects your environment — reads &lt;code&gt;style.css&lt;/code&gt; header, git remote, branch name, existing &lt;code&gt;package.json&lt;/code&gt; — and walks you through a short interview to select features.&lt;/p&gt;
&lt;h3 id=&quot;what-gets-generated&quot;&gt;What gets generated&lt;/h3&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;File&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;.releaserc.json&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Semantic Release config — the heart of automation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.github/workflows/release.yml&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Auto-release on merge to main&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.github/workflows/pr-lint.yml&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Blocks merge if PR title breaks convention&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Node.js devDependencies&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;CONTRIBUTING.md&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Developer guide with workflow reference&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;optional-features-selected-during-interview&quot;&gt;Optional features (selected during interview)&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Feature&lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Beta testing&lt;/strong&gt; (&lt;code&gt;beta.sh&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Builds a ZIP with &lt;code&gt;-beta&lt;/code&gt; directory suffix — WordPress treats it as a separate theme, so you can test stable and beta side-by-side&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ZIP assets&lt;/strong&gt; (&lt;code&gt;build-release.sh&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Creates a clean installable ZIP (no &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;, configs) attached to each GitHub Release&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub beta releases&lt;/strong&gt; (&lt;code&gt;beta-release.yml&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Manual “Run workflow” button in Actions — pick any branch, get a pre-release with ZIP attached&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;WP Admin auto-updater&lt;/strong&gt; (&lt;code&gt;includes/github-updater/&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Dashboard widget showing current vs. latest version with one-click update. Works with public and private repos&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Building a plugin instead of a theme? There’s an equivalent skill: &lt;code&gt;/wordpress-plugin-semantic-github-deployment&lt;/code&gt; — same pipeline, adapted for plugin file headers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;daily-workflow-three-commands&quot;&gt;Daily Workflow: Three Commands&lt;/h2&gt;
&lt;p&gt;Once the pipeline is set up, your daily work uses three commands from the &lt;a href=&quot;https://github.com/mateusz-zadorozny/commit-push-pr&quot; target=&quot;_blank&quot;&gt;commit-push-pr&lt;/a&gt; skill set:&lt;/p&gt;
&lt;h3 id=&quot;sco--semantic-commit&quot;&gt;&lt;code&gt;/sco&lt;/code&gt; — Semantic Commit&lt;/h3&gt;
&lt;p&gt;Analyzes your diff, picks the right type and scope, stages specific files, and commits:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;feat(search): add fuzzy matching helper&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;fix(cart): correct tax calculation to include base price&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;refactor(admin): extract settings into dedicated options handler&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Stages files by name — never &lt;code&gt;git add .&lt;/code&gt;. No secrets committed. Imperative mood, max 72 chars.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;spr--branch--commit--push--pr&quot;&gt;&lt;code&gt;/spr&lt;/code&gt; — Branch → Commit → Push → PR&lt;/h3&gt;
&lt;p&gt;The full flow in one command. Creates a semantic branch name, commits, pushes with upstream tracking, and opens a PR with the correct title:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Branch:  feat/add-search-filter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Commit:  feat(search): add result filtering&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;PR:      feat(search): add result filtering&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The PR body includes a summary and release impact type.&lt;/p&gt;
&lt;h3 id=&quot;spu--push&quot;&gt;&lt;code&gt;/spu&lt;/code&gt; — Push&lt;/h3&gt;
&lt;p&gt;Pushes the current branch to origin with upstream tracking. That’s it.&lt;/p&gt;
&lt;h3 id=&quot;why-the-pr-title-is-everything&quot;&gt;Why the PR title is everything&lt;/h3&gt;
&lt;p&gt;With &lt;strong&gt;Squash &amp;amp; Merge&lt;/strong&gt;, GitHub flattens your branch into a single commit using the PR title as the commit message. Semantic Release reads that message to decide the version bump. The PR title is the only thing that determines your next version number.&lt;/p&gt;
&lt;h3 id=&quot;conventional-commits-reference&quot;&gt;Conventional Commits Reference&lt;/h3&gt;


















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Prefix&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;th&gt;Version Change&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;feat:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;New feature&lt;/td&gt;&lt;td&gt;minor (1.x.0)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;fix:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Bug fix&lt;/td&gt;&lt;td&gt;patch (1.0.x)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;perf:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Performance improvement&lt;/td&gt;&lt;td&gt;patch&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;docs:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Documentation only&lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;style:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Code formatting&lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;refactor:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Refactoring&lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;chore:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Maintenance tasks&lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;feat!:&lt;/code&gt; / &lt;code&gt;BREAKING CHANGE:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Breaking change&lt;/td&gt;&lt;td&gt;major (x.0.0)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;the-full-flow&quot;&gt;The Full Flow&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Dev as Developer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant CC as Claude Code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant GH as GitHub&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant GHA as GitHub Actions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Rel as Release&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Dev-&amp;gt;&amp;gt;CC: /spr (or /sco + /spu)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CC-&amp;gt;&amp;gt;CC: Analyze diff → pick type &amp;amp; scope&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CC-&amp;gt;&amp;gt;GH: Create branch, commit, push, open PR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GH-&amp;gt;&amp;gt;GH: PR Lint validates title ✓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Dev-&amp;gt;&amp;gt;GH: Squash &amp;amp; Merge&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GH-&amp;gt;&amp;gt;GHA: Trigger release workflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GHA-&amp;gt;&amp;gt;GHA: Bump version in style.css&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GHA-&amp;gt;&amp;gt;GHA: Update CHANGELOG.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GHA-&amp;gt;&amp;gt;Rel: Publish v1.2.0 + ZIP asset&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;post-setup-github-settings&quot;&gt;Post-Setup: GitHub Settings&lt;/h2&gt;
&lt;p&gt;After the skill generates the files, configure your repository:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Squash merge only&lt;/strong&gt; — Settings → General → Pull Requests: enable “Allow squash merging” (default to PR title), disable merge commits and rebase merging.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Branch protection&lt;/strong&gt; — Settings → Branches: require “Validate PR title” status check before merging.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;First release gotcha:&lt;/strong&gt; On some repos, the first squash-merge may not trigger the Release workflow. If it doesn’t fire, push an empty commit directly: &lt;code&gt;git commit --allow-empty -m &amp;quot;ci: trigger release workflow&amp;quot; &amp;amp;&amp;amp; git push&lt;/code&gt;. Subsequent merges work fine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;One skill sets up the pipeline. Three commands handle the rest:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/wordpress-theme-semantic-github-deployment  →  one-time setup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/sco                                         →  semantic commit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/spr                                         →  branch + commit + push + PR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/spu                                         →  push&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your team focuses on code. The robots handle versioning, changelogs, and releases.&lt;/p&gt;
&lt;h2 id=&quot;useful-links&quot;&gt;Useful Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mateusz-zadorozny/wordpress-theme-semantic-github-deployment&quot; target=&quot;_blank&quot;&gt;wordpress-theme-semantic-github-deployment&lt;/a&gt; — Claude Code skill for setting up the release pipeline&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mateusz-zadorozny/commit-push-pr&quot; target=&quot;_blank&quot;&gt;commit-push-pr&lt;/a&gt; — Claude Code skills for semantic commits, pushes, and PRs&lt;/li&gt;
&lt;li&gt;
&lt;a href=&quot;https://semantic-release.gitbook.io/&quot; target=&quot;_blank&quot;&gt;Semantic Release — documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href=&quot;https://www.conventionalcommits.org/&quot; target=&quot;_blank&quot;&gt;Conventional Commits — specification&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href=&quot;https://github.com/amannn/action-semantic-pull-request&quot; target=&quot;_blank&quot;&gt;action-semantic-pull-request&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href=&quot;https://docs.github.com/en/actions&quot; target=&quot;_blank&quot;&gt;GitHub Actions — documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>wordpress</category><category>devops</category><category>git</category><category>github actions</category><category>automation</category><category>claude code</category></item></channel></rss>