<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nate Baldwin</title>
    <description>The latest articles on DEV Community by Nate Baldwin (@nate_baldwin_cef9a025b6ae).</description>
    <link>https://dev.to/nate_baldwin_cef9a025b6ae</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4211178%2Fc141227f-c1ee-4610-99ee-330b096e229a.png</url>
      <title>DEV Community: Nate Baldwin</title>
      <link>https://dev.to/nate_baldwin_cef9a025b6ae</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nate_baldwin_cef9a025b6ae"/>
    <language>en</language>
    <item>
      <title>Your uptime check is green and the site is broken: verifying WordPress updates across many sites</title>
      <dc:creator>Nate Baldwin</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:07:58 +0000</pubDate>
      <link>https://dev.to/nate_baldwin_cef9a025b6ae/your-uptime-check-is-green-and-the-site-is-broken-verifying-wordpress-updates-across-many-sites-3pc7</link>
      <guid>https://dev.to/nate_baldwin_cef9a025b6ae/your-uptime-check-is-green-and-the-site-is-broken-verifying-wordpress-updates-across-many-sites-3pc7</guid>
      <description>&lt;p&gt;I looked after client WordPress sites for 12 years, and the update failures that hurt most were never the ones that took a site down. A site that is down pages you within a minute. The bad ones are the update that leaves the homepage returning 200 while the checkout button is gone, the contact form quietly stops sending, or a page builder renders an empty column. The uptime monitor stays green, and the client finds it before you do.&lt;/p&gt;

&lt;p&gt;This post is the routine I settled on for verifying updates, first on one site and then across many. None of it is new. It's the boring stuff written down in one place, plus a small script you can adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why uptime checks miss update breakage
&lt;/h2&gt;

&lt;p&gt;A typical uptime check asks one question: did this URL answer with a success code within a few seconds? That's worth knowing, but most update breakage doesn't change the answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A plugin conflict breaks a JavaScript bundle. The HTML still comes back 200, but the slider, the cart or the form doesn't work.&lt;/li&gt;
&lt;li&gt;A theme or builder update changes markup or CSS. The page loads and looks wrong.&lt;/li&gt;
&lt;li&gt;A PHP warning or deprecation notice goes to a log, not the page. Nothing visible changes until a later PHP or plugin version turns it into a fatal error.&lt;/li&gt;
&lt;li&gt;A fatal error hits one template, like single product pages, while the homepage you monitor is fine.&lt;/li&gt;
&lt;li&gt;Page caching keeps serving the old, working HTML, so even a real content check on the homepage passes until the cache expires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "is it up?" is the wrong question after an update. The better question is "do the pages that matter still do what they did an hour ago?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The per-update routine
&lt;/h2&gt;

&lt;p&gt;This is what I do for each site, every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Back up right before the update, not last night.&lt;/strong&gt; If something breaks at 2 PM, a 3 AM backup means losing a morning of orders or form entries, or merging them back by hand. A backup from five minutes ago makes rollback an easy call. Save a small manifest with it (WordPress version, active plugins and their versions, table count) so you can tell later whether a restore is complete. This post on restore verification covers manifests well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Update in small batches.&lt;/strong&gt; Update 14 plugins at once, and when something breaks you have 14 suspects. I do core on its own, then the heavy plugins one at a time (WooCommerce, page builders, form plugins, anything that touches checkout or login), then the rest in small groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check key pages before and after.&lt;/strong&gt; Pick three to six URLs per site that cover the money and the main templates: homepage, contact page, a product page, cart or checkout, a blog post. Before the update, record each page's HTTP status and one string that must be on it ("Add to cart", the form's button text, a footer line). After the update, check again and compare. Before/after screenshots catch layout breakage that text checks miss. If you can do both, do both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Read the PHP error log.&lt;/strong&gt; Note where the log ends before you update, and read what's new afterwards. New fatal errors, or new warnings that name the plugin you just updated, are the early warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Decide the rollback trigger before you start.&lt;/strong&gt; Write it down so you don't argue with yourself at 6 PM. Mine is: if any key page changes status, loses its required text, or a new fatal error shows up in the log, roll back first and debug on staging. Rolling back an update you'll redo tomorrow costs very little. Debugging on a live store costs a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Rehearse the restore.&lt;/strong&gt; A backup you have never restored is a guess. Every so often, restore a real backup to staging, check it against the manifest, and time it. That time is how long a client site stays broken in your worst case.&lt;/p&gt;

&lt;h2&gt;
  
  
  WP-CLI commands that help
&lt;/h2&gt;

&lt;p&gt;If you have SSH access, WP-CLI covers most of the manifest and the update itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What you're starting from&lt;/span&gt;
wp core version
wp plugin list &lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;active &lt;span class="nt"&gt;--field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name
wp plugin list &lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="nt"&gt;--fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name,version,update_version

&lt;span class="c"&gt;# Database backup right before the update (database only, not files)&lt;/span&gt;
wp db &lt;span class="nb"&gt;export &lt;/span&gt;before-update-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d-%H%M&lt;span class="si"&gt;)&lt;/span&gt;.sql

&lt;span class="c"&gt;# Preview which plugins would be updated, without changing anything&lt;/span&gt;
wp plugin update &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Update one plugin at a time&lt;/span&gt;
wp plugin update woocommerce

&lt;span class="c"&gt;# Put one plugin back to the version you had&lt;/span&gt;
wp plugin update woocommerce &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;previous-version&amp;gt;

&lt;span class="c"&gt;# Is debug logging on? If true, the log is wp-content/debug.log&lt;/span&gt;
wp config get WP_DEBUG_LOG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few caveats. &lt;code&gt;--version&lt;/code&gt; works for plugins hosted on WordPress.org. Premium plugins usually need their old zip. &lt;code&gt;wp config get&lt;/code&gt; errors if the constant isn't defined in wp-config.php. Rolling back a plugin's files doesn't undo database changes it made during the update, which is one more reason the fresh backup matters. All of these also accept the global &lt;code&gt;--ssh=&lt;/code&gt; flag or an alias from &lt;code&gt;wp-cli.yml&lt;/code&gt;, so you can run them from your own machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal before/after page check
&lt;/h2&gt;

&lt;p&gt;This is a deliberately small example. It reads a list of URLs, each followed by a string that must appear on that page, and records the status code and whether the string was found. Run it once before the update and once after. If anything changed, it says so and exits with an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# check-pages.sh: minimal before/after check for a few key URLs.&lt;/span&gt;
&lt;span class="c"&gt;# Each line of the list file: a URL, a space, then text that must be on that page.&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./check-pages.sh pages.txt before   (run before updating)&lt;/span&gt;
&lt;span class="c"&gt;#        ./check-pages.sh pages.txt after    (run after updating)&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"results-&lt;/span&gt;&lt;span class="nv"&gt;$phase&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt;
&lt;span class="nv"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
: &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; url needle&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 20 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$needle&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else &lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$list&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$phase&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"after"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;diff results-before.txt results-after.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: same status codes and text checks as before"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"CHANGED: compare results-before.txt and results-after.txt"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a &lt;code&gt;pages.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/ Get a quote
https://example.com/contact/ Send message
https://example.com/product/blue-mug/ Add to cart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it won't do: it doesn't run JavaScript, so a broken cart script still passes. It doesn't see layout. It only sees what a logged-out visitor sees. And if page caching is on, purge it after updating, or the "after" run may be reading the old HTML. It's a floor, not a full test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling this to 50+ sites
&lt;/h2&gt;

&lt;p&gt;Doing this by hand on one site takes ten minutes. On 60 sites it falls apart unless you add some structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep an inventory.&lt;/strong&gt; One row per site: host, PHP version, WordPress version, active plugins, last maintenance date, where backups live, and the key URLs with their required text. The key URL list is the part people skip, and it's the part that makes automated checks possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track staleness with a reason.&lt;/strong&gt; "Days since last maintenance" with a color is easier to scan than a column of dates. This post on a staleness badge is a nice take on it. I'd add the reason next to the number ("32 days, 4 updates pending, 1 plugin flagged vulnerable") so you know which red site to open first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch by stack and go first on low-risk sites.&lt;/strong&gt; Group sites that share a theme, builder or WooCommerce. Update a few low-traffic sites in each group, wait a day, then do the rest. Major WordPress releases deserve a longer wait. This post on failure patterns from past major upgrades is a good read on why, including the case where the front end breaks while every admin-side check looks fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only look at what changed.&lt;/strong&gt; Loop the check over every site and collect one summary. Sixty "OK" lines are noise. You want the three sites that changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Report it.&lt;/strong&gt; A short monthly note per client listing what was updated, what was checked and what was rolled back. It takes minutes once the data exists, and it's the clearest way to show a care plan is doing real work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;Text and status checks miss visual and script breakage. Screenshots catch more, but they raise false alarms on rotating banners, cookie notices and anything with a date on it, so you'll spend some time tuning. Logged-in flows like checkout or member areas need a test account and a browser tool such as Playwright. And all of this only covers the pages you listed. Picking the right pages matters more than the tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I build WP Foreman, a hosted tool that runs this routine for agencies: a backup before every update, before/after screenshot checks with rollback if a check fails, multi-region uptime checks, SSL and domain expiry alerts, daily vulnerability scans, and branded client reports. The first site is free, then $1.99 per site per month. If you're comparing tools, I keep honest comparisons at wpforeman.com/compare. The routine works just as well with plain scripts.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>devops</category>
      <category>maintenance</category>
    </item>
  </channel>
</rss>
