Guide

WordPress PHP / core version changed unexpectedly — detecting environment drift

You log into your WordPress admin, click into Tools > Site Health, and the row that used to say PHP 8.1 now says PHP 8.2. Or you ran wp core version over SSH expecting 6.5.2 and got back 6.5.5.

1. Problem

You log into your WordPress admin, click into Tools > Site Health, and the row that used to say PHP 8.1 now says PHP 8.2. Or you ran wp core version over SSH expecting 6.5.2 and got back 6.5.5. Or a plugin that has worked fine for two years started throwing deprecation warnings everywhere this morning. The deploy log is empty. Nobody on the team pushed anything. You searched "wordpress php version changed unexpectedly" because the only honest description is: the floor moved.

This is environment drift, and on shared and managed-WordPress hosts it is depressingly common. The host did a PHP rollup on a Tuesday maintenance window. The managed-WP control panel auto-updated core because the box was checked when the site was provisioned three years ago. Someone ran wp core update against the wrong site alias. A multisite network admin pushed an update that propagated to every subsite. From your seat it looks like the site upgraded itself overnight. From the host's seat it was a routine, announced change.

Either way, the symptom is the same: the runtime your code was tested against is no longer the runtime your code is running on. You usually find out when something breaks — a payment plugin that called a removed function, a cron job that depended on a specific WP-CLI command, a theme that used a deprecated WP_Block API. The drift has been live for hours or days by then.

2. Impact

Silent environment changes are not a hygiene issue. They are a production risk because every assumption your codebase makes about the runtime is now potentially false.

Concrete impact:

  • Plugin compatibility breaks without warning. A plugin tested on PHP 8.1 may emit Deprecated notices on 8.2 that flood your logs, or fatal on 8.3 when a removed function is called. The plugin author has not changed anything. Your runtime did.
  • Security posture changes silently. A WordPress minor bump is usually safe, but a major bump (5.x to 6.x) can change REST API behavior, default user-meta capabilities, and block editor parsing. Custom code written against the old behavior is now wrong, sometimes in a way that opens an authorization gap.
  • Plugin count drift indicates account compromise. A jump from 18 active plugins to 22 overnight, when nobody installed anything, is a strong indicator that an admin credential installed a backdoor or SEO-spam injector.
  • Maintenance windows become invisible. Managed hosts rarely email about minor PHP point releases. If you do not measure your own environment from inside the site, you learn about the change when a customer reports the broken behavior.
  • Compliance evidence breaks. If an auditor asks what version of WordPress and PHP was running on date X, and you only have the host's notification email (or nothing), you cannot answer the question.
  • Multisite blast radius. On a network of 200 subsites, a single core update propagates simultaneously. One incompatible custom plugin means 200 broken sites.

3. Why It’s Hard to Spot

Several layers of WordPress and the typical hosting stack actively hide drift from operators.

  • Site Health is a snapshot, not a record. Tools > Site Health shows the current PHP and WP version. It does not show "PHP was 8.1 on Monday." There is no history. If you do not check the page, you do not know.
  • Hosts batch-announce changes — or do not announce them. Many shared hosts publish a quarterly "supported PHP versions" page and migrate sites silently within that window. Managed-WP providers often email about major changes but not patch versions. PHP 8.2.18 → 8.2.20 typically lands with no notification.
  • Auto-update is on by default. Since WordPress 5.6, automatic updates for both core and plugins are enabled by default for new installs. Most owners have never touched the setting and do not know which plugins auto-update.
  • Uptime checks pass through it. A status-200 from the homepage does not change when PHP goes from 8.1 to 8.2. Pingdom, UptimeRobot, and StatusCake all show green through a runtime swap.
  • Logs without context look normal. A spike in deprecation notices in php-fpm log can mean a plugin update, a core update, a PHP bump, or all three. Without an environment-state signal at the same timestamp, you cannot tell which.
  • Plugin-count changes have no UI surface. WordPress notices when you install a plugin. It does not notice that the plugin list grew between yesterday and today. A compromised admin adding a backdoor plugin appears only as a new row nobody is watching.
  • Multisite network admins are invisible to subsite admins. A super-admin can install or update plugins network-wide. The subsite admin sees the result but receives no notification.

4. Cause

WordPress does not, by default, tell you when its environment changes. It exposes the current state — phpversion(), get_bloginfo('version'), get_plugins() — but not the delta. There is no WP_Cron hook for "PHP was upgraded." There is no admin notice for "plugin count changed since yesterday."

The Logystera WP plugin closes that gap by sampling the environment on every request (cheaply, from already-loaded values) and emitting two complementary signals.

wp_environment_info is a gauge — a labeled time series carrying the current state: php_version, wp_version, plugin_count, multisite, theme, db_version. It does not tell you whether anything changed. It tells you what the value is right now.

wp_environment_changes_total is a counter — it increments once each time the plugin observes that one of those values differs from the last value it saw for this site. The counter has a field label (php_version, wp_version, plugin_count, multisite, theme) and a direction label (upgrade, downgrade, change) so you can tell whether PHP went up, WordPress went down, or the active theme was swapped.

The plugin keeps the previous values in site option storage, compares on each sample, and emits the delta. When wp_environment_changes_total{field="php_version"} increments at 03:14 UTC and wp_environment_info{php_version="8.2.18"} replaces the previous wp_environment_info{php_version="8.1.27"} in the same window, you have a timestamped record: PHP changed from 8.1.27 to 8.2.18, observed by the site itself, at this minute.

Supporting signals fill in the picture. wp_environment_plugin_count is a gauge of currently active plugins. wp_environment_multisite is a 0/1 gauge that flips when a single-site WordPress is converted to a network. Together they give you a fingerprint of the runtime that updates in near real time.

5. Solution

5.1 Diagnose (logs first)

When you suspect drift — or when wp_environment_changes_total has incremented and you want to know what changed — go directly to the signal and the supporting evidence on the host.

1. Confirm what changed and when, using the primary signal. Query:

increase(wp_environment_changes_total{entity_id="<site>"}[7d])

This gives you, per field and direction label, the count of changes over the last seven days. A non-zero value for field="php_version" is the answer to "did PHP change in the last week." This is the primary indicator from wp_environment_changes_total.

2. Get the before/after values from the gauge. The counter says "it changed." wp_environment_info says "from what to what":

wp_environment_info{entity_id="<site>"}[7d]

The label set on the series before the increment is the old value; the label set after is the new value. PHP 8.1.27 → 8.2.18 is now a hard fact with a timestamp, sourced from wp_environment_info.

3. Confirm from inside the site over SSH. SSH to the host and pull the current values directly:

wp core version --allow-root
wp eval 'echo PHP_VERSION;'
wp plugin list --status=active --format=count
wp option get siteurl

If wp core version differs from what you expect, or from what your deploy artifact records, the WP core was updated outside your deploy pipeline. If the active plugin count differs from your IaC plugin manifest, plugins were added or activated outside your pipeline.

4. Check who or what triggered the change. Look at the last-modified timestamp on the WordPress core files and the active plugins:

ls -la /var/www/html/wp-includes/version.php
find /var/www/html/wp-content/plugins -maxdepth 2 -name "*.php" -newer /tmp/yesterday -ls | head -50

Files newer than your last deploy mean something else wrote to them. On a managed host, this is usually the host's auto-update worker. On a self-hosted box, it is wp-cron, an admin user via the dashboard, or a compromised account.

5. Check WordPress's own update log. The auto-update mechanism writes to a transient and the update_core option:

wp option get auto_update_core_dev
wp eval 'print_r(get_site_transient("update_core"));'
wp option get core_updater.lock

A last_checked timestamp matching when wp_environment_changes_total{field="wp_version"} incremented confirms the source was WordPress's own auto-updater.

6. Check PHP-FPM and host logs for the version swap event. On most managed and shared hosts, a PHP version change is recorded in the FPM startup log:

grep -E "starting|NOTICE: fpm is running" /var/log/php*-fpm.log | tail -20
journalctl -u php8.2-fpm --since "2 days ago" | grep -E "starting|stopping"

A starting line for php8.2-fpm with no matching prior stopping line for php8.1-fpm means the host swapped the upstream pool. That timestamp should match wp_environment_changes_total{field="php_version"}.

7. For plugin-count drift, diff the plugin list. If wp_environment_changes_total{field="plugin_count"} incremented and you do not know which plugin appeared:

wp plugin list --format=csv > /tmp/plugins-now.csv
diff /tmp/plugins-baseline.csv /tmp/plugins-now.csv

A new row that nobody on the team installed is the artifact you are looking for. This is the same signal that drives intrusion detection for backdoor plugins — wp_environment_changes_total{field="plugin_count", direction="upgrade"} without a corresponding deploy is suspicious by default.

5.2 Root Causes

(see root causes inline in 5.3 Fix)

5.3 Fix

Drift detection has two outcomes: the change was acceptable, or it was not. The fix depends on which.

Cause 1: Host auto-rolled a PHP minor version (8.1.27 → 8.1.30). Signal: wp_environment_changes_total{field="php_version", direction="upgrade"} increments, and the major.minor stays the same. This is almost always safe and intended. Verify your plugins are still loading without Deprecated notices in php-fpm log, and update your IaC/runbook to record the new patch version. No code change needed.

Cause 2: Host rolled a PHP major or minor version (8.1 → 8.2). Signal: same counter, but the major or minor part of the version changed. Run your test suite against the new PHP. Check WordPress.org's plugin-compatibility tags for every active plugin against the new version. If one of them lists "Tested up to: PHP 8.1" only, contact the plugin author or pin to the previous PHP via your host's control panel until you have a tested upgrade path.

Cause 3: WordPress core auto-updated. Signal: wp_environment_changes_total{field="wp_version"}. Decide whether you want this. To pin core versions, set in wp-config.php:

define('WP_AUTO_UPDATE_CORE', 'minor'); // or false for full lock
define('AUTOMATIC_UPDATER_DISABLED', true); // disable all auto-updates

Then re-run wp core check-update to confirm the version is held. If you let it through, regression-test the site against the new WP version and update your deploy artifact's expected version.

Cause 4: Managed-WP control panel auto-updated WordPress. Signal: wp_environment_changes_total{field="wp_version"} with no deploy entry and WP_AUTO_UPDATE_CORE already set. The wp-config.php constant does not override managed-host panels. Disable WordPress auto-updates at the control-panel level.

Cause 5: Accidental wp core update against the wrong alias. Signal: wp_environment_changes_total{field="wp_version"} correlated to a wp-cli invocation in shell history. Roll back with wp core download --version= --force and wp core update-db, then audit your ~/.wp-cli/config.yml aliases.

Cause 6: Plugin count grew without a deploy. Signal: wp_environment_changes_total{field="plugin_count", direction="upgrade"} with wp_environment_plugin_count jumping. Diff the plugin list (step 7 above), identify the new plugin, and decide whether it was legitimate. If not, treat as credential compromise: rotate admin passwords, audit wp_users.user_registered, and inspect the plugin's PHP files for backdoor patterns (base64-encoded eval, assert($_POST[...]), etc.).

Cause 7: Multisite conversion. Signal: wp_environment_multisite flips from 0 to 1. This is never accidental — someone defined WP_ALLOW_MULTISITE and ran the network setup. If it was not you, treat as an unauthorized configuration change.

5.4 Verify

The metric you want to see is silence — wp_environment_changes_total should not increment again until the next intentional change.

After locking the environment (Cause 3 / 4 above), check:

increase(wp_environment_changes_total{entity_id="<site>"}[24h])

A value of 0 across all field labels for 24 hours under normal traffic means the lock holds. A non-zero value within minutes means your lock did not stick — typically because the managed host overrides wp-config.php constants.

For ongoing health, the gauge should be flat:

wp_environment_info{entity_id="<site>"}

The label set should not change between samples. A change is the failure mode.

For php-fpm specifically, deprecation noise should drop back to baseline once you have either rolled forward (updated plugins) or rolled back (pinned PHP):

grep -c "Deprecated" /var/log/php-fpm.log

Compare hourly counts before and after. A return to pre-drift baseline confirms the runtime is back in sync with the code.

If wp_environment_changes_total increments again within a week without an entry in your deploy log, the lock is broken and you need to escalate to the host. Do not assume the change is benign just because the site still loads.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as wp_environment_changes_total.

The hard part is not fixing drift. It is knowing it happened. WordPress does not surface this. Site Health is a snapshot. Hosts do not always notify. Uptime checks pass through. Without instrumentation, you discover drift by tripping over the broken behavior — payment plugin fatals, REST API returns wrong shape, customer files a ticket.

This failure surfaces as wp_environment_changes_total, which Logystera detects on the next sample after the change occurs and alerts on within minutes. The supporting wp_environment_info gauge gives you the before/after values without you needing to grep file timestamps. The wp_environment_plugin_count gauge surfaces backdoor installs that the WordPress UI will never alert you about. Together they convert "find out when something breaks" into "find out when the floor moves, before anything breaks."

A useful detection pattern: alert on any non-zero increase(wp_environment_changes_total[5m]) that does not correlate to an entry in your deploy log. Legitimate deploys change versions; everything else is drift, and drift always deserves a human look.

7. Related Silent Failures

Environment drift is one face of a broader cluster of "WordPress changed and nobody told you" failures. If wp_environment_changes_total is firing, check these neighbors:

  • Plugin auto-update breakage — a plugin updated overnight via auto_update_plugins, broke a hook, surfaces as php.fatal correlated with wp.state_change type=plugin_updated.
  • REST API behavior change after WP minor bumpwp_environment_changes_total{field="wp_version"} followed by http.request 5xx spikes on /wp-json/ routes.
  • Theme swapwp_environment_changes_total{field="theme"} with no deploy entry; usually accidental click in admin, sometimes a defacement.
  • Backdoor plugin installwp_environment_plugin_count jumps without deploy, correlated with auth.attempt success from an unusual IP in the preceding hour.
  • Multisite network-wide updatewp_environment_changes_total increments on dozens of subsite entities at the same minute; investigate the network admin action log.

See what's actually happening in your WordPress system

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.