WordPress PHP Version Incompatibility: Check and Upgrade Without Downtime

Was this helpful?

The scariest WordPress outage is the quiet one: a PHP upgrade “for security” that passes CI, looks fine in a browser,
and then turns your highest-revenue checkout flow into a white screen at 2 a.m. while your on-call tries to remember
which pool is on which host. Everyone has opinions. Logs have facts.

PHP version incompatibility isn’t mysterious. It’s just a mismatch between runtime behavior and code assumptions—
in WordPress core, in plugins, in themes, and in your stack glue (opcache, object cache, proxies). You can upgrade
without downtime if you treat it like a production change: inventory, test with the real traffic shape, switch safely,
and keep rollback boring.

What actually breaks when PHP versions change (and why WordPress feels it)

A PHP upgrade is not “just a new interpreter.” It’s a change in language rules, standard library behavior, default
configuration, and the extensions that WordPress depends on. WordPress sits at the crossroads of legacy code and
modern hosting. It supports old sites, plugins from 2012, and developers who treat functions.php like a
confessional. That’s why it’s sensitive.

Compatibility failures come in four flavors

  1. Hard failures: fatal errors, parse errors, missing functions/classes, missing extensions.
    These are the “site is down” moments.
  2. Soft failures: warnings/notices that become fatals under stricter settings, or logic changes that
    flip behavior (empty strings vs nulls, comparisons, array handling).
  3. Performance regressions: opcache differences, JIT side effects, or code paths that become slower
    due to engine changes. Usually shows up as increased CPU and tail latency, not immediate errors.
  4. Operational mismatches: different php.ini defaults, different FPM pool configs,
    missing system packages, changed file permissions, or one server upgraded and the other not.

Why WordPress upgrades “work in staging” and still fail in production

Staging often has a different plugin set, different caching, and polite traffic. Production traffic is rude.
It hits strange endpoints, sends big cookies, triggers cron at awkward times, and exercises rarely used admin pages.
And most staging environments don’t run the same object cache, the same file system semantics, or the same opcode cache
warm state.

One operational quote worth keeping on a sticky note: “Hope is not a strategy.” — paraphrased idea commonly
attributed in reliability circles (often linked to Gordon R. Dickson). In this context, hope means “upgrade and see.”
Don’t. Measure and switch with a plan.

Also, WordPress is a plugin ecosystem. A single plugin can pull in a vendor library that assumes one PHP version’s
behavior. You upgrade PHP and suddenly a dependency starts throwing TypeError where it used to quietly
cast things. That’s not PHP being mean; that’s PHP being explicit. Your error budget may disagree.

Joke #1: Upgrading PHP without checking plugins is like changing the lock on your front door and acting surprised when
your keys stop working.

Interesting facts & historical context (the stuff that explains the pain)

  • PHP 7.0 (2015) was the big performance pivot after the PHPNG work; many WordPress sites saw dramatic speedups without changing code.
  • PHP 5.6 reached end of life in 2018, but shared hosting kept it alive in the wild for years because “nothing broke yet.”
  • PHP 8.0 (2020) introduced the JIT, but typical WordPress workloads rarely benefit much; you mostly care about engine improvements and compatibility.
  • PHP 8 tightened typing behavior and made more operations throw TypeError instead of limping along; many legacy plugins relied on that limping.
  • MySQL extension history matters: long ago WordPress used mysql_* functions; modern stacks use mysqli or PDO. Old plugins sometimes didn’t get the memo.
  • WordPress’s “white screen of death” became a meme because fatals were often hidden by display_errors=Off; modern WordPress has recovery mode, but it’s not a magic shield.
  • OPcache became mainstream in PHP 5.5; today, most performance stability depends on tuning opcache and not thrashing it with constant deploys.
  • Composer wasn’t always common in WordPress land; many plugins still bundle vendor code manually, which makes dependency patching and PHP compatibility more chaotic.
  • FPM replaced mod_php as the standard for serious deployments because process management and isolation get saner—also because running everything in Apache is a lifestyle choice.

Fast diagnosis playbook: confirm it’s PHP, then find the edge

When a WordPress site starts throwing 502s, blank pages, or weird admin failures after “a routine maintenance window,”
you need a tight loop: confirm where the failure is, pinpoint the code path, and decide whether to rollback or patch.
Don’t spelunk randomly.

First: confirm the symptom layer

  1. Is it HTTP-level (502/504) or app-level (200 with broken HTML)?
    502 usually means PHP-FPM died, hung, or upstream is miswired. 200 with blank page often means PHP fatal with suppressed output.
  2. Is the failure on all nodes or just some?
    Mixed PHP versions across a load-balanced pool are a classic “works sometimes” failure mode.
  3. Is it limited to admin, cron, or a specific plugin route?
    Checkout flows and AJAX endpoints often hit different code than your homepage.

Second: check the two logs that actually matter

  1. PHP-FPM error log (or systemd journal): shows fatals, segfaults, and pool config issues.
  2. WordPress/PHP application log: wp-content/debug.log if enabled, or your centralized logging.

Third: isolate whether it’s incompatibility or capacity

  • If errors mention Call to undefined function, Class not found, Parse error, or TypeError after an upgrade: treat it as incompatibility until proven otherwise.
  • If the logs show timeouts, server reached pm.max_children, or long request durations: your upgrade changed performance characteristics, and you’re now capacity-bound.
  • If failures are intermittent and correlate with a specific backend: you likely have a version skew or config skew.

Fourth: decide rollback vs forward-fix

My rule: if checkout/admin is down or error rates are spiking and you don’t have a confirmed fix in hand, rollback first.
A rollback buys you thinking time without your customers paying for your curiosity.

Hands-on tasks (commands, outputs, and decisions)

These are the tasks I actually run. Not because they’re fancy, but because they close ambiguity.
Each task includes: command, what output means, and the decision you make.

Task 1: Identify the PHP version serving the site (CLI vs FPM can differ)

cr0x@server:~$ php -v
PHP 8.1.2 (cli) (built: Feb 15 2025 10:41:12) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.2, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.2, Copyright (c), by Zend Technologies

Meaning: This is the CLI binary. It might not match what Nginx/Apache uses via FPM.
Decision: If you’re debugging web behavior, don’t assume this is the runtime. Check FPM next.

Task 2: Confirm PHP-FPM version and status via systemd

cr0x@server:~$ systemctl status php8.1-fpm --no-pager
● php8.1-fpm.service - The PHP 8.1 FastCGI Process Manager
     Loaded: loaded (/lib/systemd/system/php8.1-fpm.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2025-12-27 10:12:33 UTC; 2h 4min ago
       Docs: man:php-fpm8.1(8)
   Main PID: 1234 (php-fpm8.1)
     Status: "Processes active: 6, idle: 10, Requests: 24912, slow: 3, Traffic: 1.2req/sec"

Meaning: FPM is running and reporting slow requests.
Decision: If you see “failed” or restart loops, you’re in outage territory. If slow requests spike after upgrade, investigate performance and timeouts.

Task 3: Verify what upstream socket Nginx is using (catch version skew)

cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '/fastcgi_pass/p' | head
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;

Meaning: Nginx is wired to the PHP 8.1 socket.
Decision: If this points to php7.4-fpm.sock on some nodes and php8.1-fpm.sock on others, fix your config management before touching code.

Task 4: Confirm Apache PHP handler (mod_php vs proxy_fcgi)

cr0x@server:~$ apachectl -M 2>/dev/null | egrep 'php|proxy_fcgi|mpm'
 mpm_event_module (shared)
 proxy_fcgi_module (shared)

Meaning: Apache is likely using FPM via proxy_fcgi, not mod_php.
Decision: If you see a loaded php_module, you’re on mod_php and version switching is different and riskier. Prefer FPM isolation for multi-version setups.

Task 5: Hit a local phpinfo-style endpoint safely (without exposing it publicly)

cr0x@server:~$ curl -sS -H 'Host: example.com' http://127.0.0.1/wp-admin/admin-ajax.php | head
0

Meaning: This doesn’t show PHP version, but it confirms WordPress executes via the web path locally.
Decision: If this returns 502/504 locally, the issue is on the server/app path, not the CDN or external DNS.

Task 6: Enable WordPress debugging (temporarily) and confirm log writes

cr0x@server:~$ sudo -u www-data php -r 'echo "ok\n";'
ok

Meaning: Your web user can run PHP and should be able to write logs if permissions allow.
Decision: If the web user can’t execute or write where needed, fix permissions before you chase “compatibility” ghosts.

Task 7: Tail PHP-FPM logs for fatals and pool errors during a request

cr0x@server:~$ sudo tail -n 30 /var/log/php8.1-fpm.log
[27-Dec-2025 12:14:22] WARNING: [pool www] server reached pm.max_children setting (20), consider raising it
[27-Dec-2025 12:14:27] ERROR: WARNING: [pool www] child 1842 said into stderr: "PHP Fatal error:  Uncaught TypeError: strlen(): Argument #1 ($string) must be of type string, null given in /var/www/html/wp-content/plugins/foo/bar.php:91"

Meaning: You have both capacity pressure (pm.max_children) and a real incompatibility (TypeError).
Decision: Fix the fatal first (it’s correctness). Then address capacity, because errors can amplify load via retries.

Task 8: Confirm installed PHP modules (missing extensions cause weirdness)

cr0x@server:~$ php -m | egrep 'curl|gd|imagick|mbstring|mysqli|openssl|zip'
curl
gd
mbstring
mysqli
openssl
zip

Meaning: Modules like imagick might be missing after an upgrade, changing media processing behavior.
Decision: If a required module is missing, install it for the target PHP version and restart FPM before blaming WordPress.

Task 9: Check WordPress core and plugin versions with WP-CLI

cr0x@server:~$ cd /var/www/html
cr0x@server:/var/www/html$ sudo -u www-data wp core version
6.4.3

Meaning: You have core version pinned.
Decision: If core is old relative to your target PHP, upgrade core in staging first. Core compatibility is generally better than plugin compatibility, but don’t gamble.

Task 10: Find the plugins most likely to break (and disable one without touching the DB manually)

cr0x@server:/var/www/html$ sudo -u www-data wp plugin list --status=active
+-----------------------+----------+-----------+---------+
| name                  | status   | update    | version |
+-----------------------+----------+-----------+---------+
| woocommerce           | active   | available | 8.1.1   |
| elementor             | active   | none      | 3.18.0  |
| foo-payments-gateway  | active   | none      | 2.4.7   |
+-----------------------+----------+-----------+---------+

Meaning: You now have a shortlist of code that executes on most requests.
Decision: If a fatal points into a plugin path, disable that plugin on a test node or staging first. In emergency, disable on production only if you understand the business impact.

Task 11: Run a PHP syntax check across a plugin or theme (catches parse errors early)

cr0x@server:/var/www/html$ find wp-content/plugins/foo-payments-gateway -name '*.php' -print0 | xargs -0 -n1 php -l | head
No syntax errors detected in wp-content/plugins/foo-payments-gateway/includes/api.php
No syntax errors detected in wp-content/plugins/foo-payments-gateway/foo.php

Meaning: No parse errors. That’s necessary, not sufficient.
Decision: If you see parse errors, that plugin is dead on that PHP version. Upgrade/replace it, or don’t upgrade PHP yet.

Task 12: Detect deprecated calls and warnings by running a page under stricter error reporting (staging)

cr0x@server:~$ php -d display_errors=1 -d error_reporting=E_ALL -r 'trigger_error("test", E_USER_DEPRECATED);'
Deprecated: test in Command line code on line 1

Meaning: You can force visibility of deprecations in a controlled environment.
Decision: In staging, crank error visibility and run critical flows. Deprecations can be tomorrow’s fatals when libraries update.

Task 13: Check live traffic errors from access logs (spot 502 spikes and failing endpoints)

cr0x@server:~$ sudo awk '$9 ~ /^5/ {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
  184 /wp-admin/admin-ajax.php
   77 /?wc-ajax=checkout
   22 /wp-json/wc/v3/orders

Meaning: The failures cluster around AJAX and checkout—classic plugin-heavy paths.
Decision: Focus testing and isolation on those routes. Don’t waste time refreshing the homepage.

Task 14: Confirm opcache settings (mis-tuned opcache can look like “PHP upgrade broke it”)

cr0x@server:~$ php -i | egrep 'opcache.enable|opcache.memory_consumption|opcache.max_accelerated_files' | head
opcache.enable => On => On
opcache.memory_consumption => 128 => 128
opcache.max_accelerated_files => 10000 => 10000

Meaning: OPcache is enabled, but memory may or may not be enough depending on plugin sprawl.
Decision: If you see frequent cache resets or “out of memory for opcache,” increase opcache memory and file slots. Otherwise you’ll chase phantom latency.

Task 15: Compare PHP versions across a fleet (catch partial upgrades)

cr0x@server:~$ for h in wp-app-01 wp-app-02 wp-app-03; do ssh $h "php-fpm8.1 -v 2>/dev/null | head -1 || php-fpm8.0 -v 2>/dev/null | head -1"; done
PHP 8.1.2 (fpm-fcgi) (built: Feb 15 2025 10:41:12)
PHP 8.1.2 (fpm-fcgi) (built: Feb 15 2025 10:41:12)
PHP 8.0.30 (fpm-fcgi) (built: Jan 10 2025 08:12:44)

Meaning: One node is on PHP 8.0 while others are on 8.1.
Decision: Stop. Fix version skew before investigating “random” errors. Mixed runtimes are a reliability tax you pay forever.

Task 16: Verify what config file FPM is using (because you think you changed it…)

cr0x@server:~$ php-fpm8.1 -tt 2>&1 | head -20
[27-Dec-2025 12:33:01] NOTICE: configuration file /etc/php/8.1/fpm/php-fpm.conf test is successful
[27-Dec-2025 12:33:01] NOTICE: using php.ini in /etc/php/8.1/fpm/php.ini

Meaning: You’re editing the right file path for FPM.
Decision: If it’s pointing somewhere else (containers, custom prefixes), adjust your deployment tooling. Editing the wrong ini file is an evergreen classic.

Task 17: Validate PHP-FPM pool capacity and timeouts (avoid “upgrade caused 502” myths)

cr0x@server:~$ sudo egrep 'pm\.max_children|pm\.max_requests|request_terminate_timeout' /etc/php/8.1/fpm/pool.d/www.conf | sed 's/;.*$//'
pm.max_children = 20
pm.max_requests = 500
request_terminate_timeout = 60s

Meaning: Your pool is capped at 20 workers and kills requests at 60 seconds.
Decision: If traffic or slower code increased request time, raise max_children (within CPU/RAM) and tune terminate timeout. But don’t “solve” fatal errors by raising timeouts.

Upgrade patterns that avoid downtime (Nginx/Apache, PHP-FPM, and rollbacks)

“Without downtime” doesn’t mean “no risk.” It means you can switch runtime versions without dropping connections or
taking the site dark. The trick is parallelism: run old and new PHP side by side, route traffic intentionally, and
keep rollback one config change away.

Pattern A: Parallel PHP-FPM sockets + phased traffic shift

Run two FPM services: old (e.g., PHP 8.0) and new (e.g., PHP 8.1). Wire Nginx (or Apache proxy_fcgi) to one socket at a time,
or better, split traffic at the load balancer: canary one node on new PHP, keep the rest on old.

This is the cleanest approach for near-zero downtime because:

  • It decouples package installation from traffic switching.
  • It allows real production traffic testing on a small slice.
  • Rollback is fast: flip the upstream socket or remove the canary from the pool.

Pattern B: Blue/green app tier with immutable images

Build a new image (VM or container) with the new PHP runtime, same configs, same WordPress code and plugins, then bring it
up behind the load balancer next to the old pool. Drain and switch. This is how you keep changes auditable.

It also prevents “snowflake servers” where one node has a different extension installed because someone debugged something
once and never wrote it down.

Pattern C: Same servers, but fast rollback via config toggle

If you must switch in place, keep the old FPM service installed and enabled, and treat the switch as a config deploy:
update Nginx fastcgi_pass to the new socket and reload. Reload, not restart.

Reload means existing connections survive; new workers pick up the config. Restart means you learn about angry clients.
Choose reload. Always.

The minimum safe rollback plan

  • Old PHP-FPM remains installed and running (or startable) during the entire upgrade window.
  • Your web server can flip between sockets or upstreams quickly.
  • OPcache is reset on switch (or you accept mixed cached bytecode risk).
  • You know which plugins/themes changed recently, and you can disable them with WP-CLI.

Joke #2: The only thing more permanent than a temporary PHP upgrade is the “temporary” hotfix you’ll be afraid to delete.

Storage and state: the hidden downtime multiplier

WordPress isn’t just PHP. It’s PHP plus a database plus uploads plus caches. A PHP upgrade can trigger:

  • Cache stampedes if opcache resets and object caches miss at once.
  • Media regeneration changes if image libraries differ (gd vs imagick availability).
  • Filesystem permission shifts if packages change user/group defaults or pool configs.

If you run shared storage (NFS, EFS, Gluster, CephFS), be careful: a plugin doing lots of stat() calls can
become slower under a new PHP runtime (or just under different opcache warm state), and your storage latency suddenly
matters. In production, storage is never “someone else’s problem.” It’s just a problem that hasn’t learned your phone number yet.

Three corporate-world mini-stories (pain, hubris, and boring competence)

Mini-story #1: The incident caused by a wrong assumption

A mid-sized company ran WordPress for marketing pages and a separate app for billing. Marketing wasn’t “critical,”
which is a phrase executives say right before requesting an ETA every seven minutes.

The infrastructure team upgraded PHP from 7.4 to 8.1 on the web nodes during a routine patch cycle. They validated
php -v, refreshed the homepage, saw a 200, and closed the ticket. The assumption was simple: “If the homepage loads, WordPress is fine.”

The next morning, the content team tried to publish a post. The editor crashed. Then the admin login started throwing
a 500. The homepage still looked fine because it was cached at the CDN and the edge cache didn’t care about PHP fatals.
Meanwhile, scheduled posts didn’t publish because WP-Cron calls were failing, and a campaign launch quietly missed its window.

The root cause was a plugin used only in the admin editor path. It used a library that triggered a TypeError
under PHP 8.1 when passed null. It never executed on anonymous homepage requests. Nobody tested the admin path. Nobody
checked logs. Everyone tested the one page most likely to be cached.

The fix was embarrassingly straightforward: pin the plugin to an updated version and add an “admin critical path” test
to the release checklist. The real lesson was operational: validate the workflows that matter, not the pages that are convenient.

Mini-story #2: The optimization that backfired

Another organization wanted to “speed up WordPress” and decided to enable aggressive OPcache settings and a larger
pm.max_children right after upgrading PHP. Two changes at once. Two dials, one dashboard. You can guess
how this ends.

Initially, metrics looked great: higher throughput, fewer CPU spikes. Then memory pressure crept up. The nodes began
swapping. Latency went nonlinear. A few 502s appeared, then a lot. The team assumed the new PHP version was unstable.

It wasn’t. They had increased worker counts without increasing memory, and OPcache memory was set large enough that
each node had less headroom for actual request workloads. Under load, the kernel started reclaiming memory, the page
cache got churned, and PHP workers stalled. The stack didn’t “crash.” It suffocated.

The rollback to the old PHP version didn’t help because the resource tuning stayed in place. That created confusion:
“We rolled back but it’s still broken.” After an hour of digging, they reverted the FPM pool and OPcache settings and the site recovered.

The lesson: performance tuning is a change with blast radius. Do it separately from a version upgrade. And if you
touch pm.max_children, you’d better know your per-worker memory footprint, not just your CPU usage.

Mini-story #3: The boring but correct practice that saved the day

A third company ran WooCommerce with a lot of plugins, some ancient, some bespoke. They had a habit that looked
bureaucratic: every runtime upgrade required a canary node and a scripted “top 20 endpoints” synthetic test.
No exceptions.

The PHP upgrade to 8.2 was staged. They built new images, brought up a single canary node, and routed 2% of traffic to it.
The synthetic tests hit checkout, add-to-cart, login, admin post save, and a few API endpoints. They also mirrored real
traffic patterns by replaying a small sample of access logs against the canary (sanitized and rate-limited).

Within minutes, error logs on the canary showed warnings turning into fatals inside a shipping plugin. The main pool remained healthy.
The team disabled the plugin on the canary to confirm the diagnosis, then rolled the canary back out of rotation.
Customers never noticed.

The “boring” part was the runbook: the exact commands to diff configs, compare module lists, validate pool settings,
and revert traffic weights. It wasn’t clever. It was repeatable. That’s what saved them.

Their later fix was also boring: replace the shipping plugin with a maintained alternative and schedule the PHP upgrade again.
They lost a day, not a weekend.

Common mistakes: symptom → root cause → fix

If you’ve been in production long enough, you can diagnose half of these from a single screenshot. The other half need logs.
Here’s the map.

1) Symptom: White screen (200 OK, blank page)

  • Root cause: PHP fatal error with display_errors=Off, often in theme or plugin initialization.
  • Fix: Check PHP-FPM log for fatals. Enable WordPress debug logging temporarily. Identify and update/disable the offending plugin/theme.

2) Symptom: 502 Bad Gateway right after upgrade

  • Root cause: Nginx/Apache still points to the old socket path, or FPM isn’t running, or the pool can’t start due to config syntax.
  • Fix: Confirm fastcgi_pass target, run php-fpm -tt, check systemd status, correct socket permissions, reload web server.

3) Symptom: Works sometimes, fails sometimes

  • Root cause: Version skew across nodes; one server upgraded, others not. Or inconsistent plugin files on shared storage.
  • Fix: Compare runtime versions on all nodes. Ensure code deployments are atomic and identical; don’t mix NFS hand-edits with CI deploys.

4) Symptom: Admin dashboard broken, public site fine

  • Root cause: Incompatible plugin used primarily in admin/editor paths. CDN hides public failures.
  • Fix: Test admin workflows in staging. Watch canary logs. Disable/update admin-only plugins first.

5) Symptom: Checkout fails, cart page fine

  • Root cause: Payment gateway or shipping plugin incompatibility; stricter typing in PHP 8 triggers fatals in rarely used edge cases.
  • Fix: Reproduce via /?wc-ajax=checkout, read PHP errors, update the plugin, or replace it. Don’t “just increase timeouts.”

6) Symptom: Sudden CPU spike and slow requests after upgrade

  • Root cause: OPcache cold start, JIT misconfiguration, or reduced effectiveness of caching due to resets; sometimes a plugin hits a slower path under new PHP.
  • Fix: Warm caches, confirm opcache settings, check slowlog, and compare request profiles. Adjust FPM pool only after correctness is restored.

7) Symptom: Image uploads fail or thumbnails missing

  • Root cause: Missing gd or imagick on the new PHP version; or different library versions change behavior.
  • Fix: Install matching extensions, restart FPM, verify with php -m, then retest uploads.

8) Symptom: “Allowed memory size exhausted” appears more often

  • Root cause: Different default memory limits in FPM ini; plugin behavior changes; increased concurrency increases peak memory.
  • Fix: Confirm memory_limit for FPM, tune it with real constraints, and audit the plugin doing the allocation. Do not paper over leaks with infinite memory.

Checklists / step-by-step plan

This is the plan I’d run for a production WordPress site where downtime is unacceptable and blame is abundant.
Follow the order. The order is the whole point.

Phase 0: Decide what “no downtime” means for you

  • Target: No planned maintenance page; existing sessions survive; brief tail latency increase acceptable.
  • Non-negotiables: rollback in minutes, canary, and log visibility.
  • Reality check: If you run a single server with no load balancer, “no downtime” becomes “very short downtime.” You can still be careful, but physics wins.

Phase 1: Inventory the runtime, modules, and WordPress surface area

  • Record PHP-FPM version, ini path, module list, opcache settings.
  • Export plugin list and theme.
  • Identify critical flows: login, admin save, checkout, API endpoints, cron.
  • Confirm you can disable plugins via WP-CLI if wp-admin becomes inaccessible.

Phase 2: Build a staging environment that is not a fairy tale

  • Same WordPress code, same plugins, same theme.
  • Same caching layers (object cache, page cache behavior), or at least understand differences.
  • Copy production database (sanitized) and a representative chunk of uploads.
  • Use the same PHP-FPM config class (timeouts, pool settings) so you catch capacity issues early.

Phase 3: Compatibility testing that actually finds problems

  • Run top endpoints and critical workflows under the new PHP.
  • Turn warnings into signal in staging: high error reporting, log capture.
  • Watch for TypeErrors, deprecated warnings in key paths, missing extensions.
  • Run a targeted plugin syntax scan and smoke tests on plugins known to be risky (payment, shipping, builders).

Phase 4: Canary in production

  • Bring up one node with new PHP (or switch one existing node).
  • Route small traffic percentage to it.
  • Monitor: 5xx rate, latency, PHP-FPM restarts, slowlog, memory usage.
  • If errors appear: remove canary from rotation, fix forward in staging, try again.

Phase 5: Rollout and post-switch stabilization

  • Roll out across the fleet gradually.
  • Warm caches to avoid stampedes.
  • Keep old PHP available until you’re through a full business cycle (not just 15 calm minutes).
  • After stability: remove old runtime, but keep rollback artifacts documented (how to reinstall quickly, which packages).

FAQ

1) How do I know if a WordPress issue is PHP incompatibility or just a capacity problem?

Look for fatals and TypeErrors in PHP-FPM logs. Capacity issues show as timeouts, “reached pm.max_children,” slow requests, and 502s without a specific stack trace.
Incompatibility almost always leaves a breadcrumb: file path, line number, and a clear error type.

2) Can I upgrade PHP without upgrading WordPress core?

Sometimes, but it’s a bad bet. Core generally keeps up with supported PHP versions, but old core plus modern PHP increases the risk that plugins hit untested paths.
Upgrade core in staging first, then PHP. If you can’t, at least validate your exact core version against your target PHP in staging with real workflows.

3) Why does WP-CLI work but the website fails?

WP-CLI uses the CLI SAPI and its own php.ini path. Your site uses PHP-FPM with different ini settings, different extensions, and different user permissions.
Always check both. The difference is not academic; it’s where outages live.

4) Is it safe to run two PHP-FPM versions on the same server?

Yes, if you isolate sockets/ports and configs per version. It’s a common operational pattern for staged upgrades.
The risk is operator error: pointing Nginx to the wrong socket, or installing modules on one version and forgetting the other. Use config management and explicit paths.

5) What’s the fastest rollback if PHP 8.x breaks a plugin?

If you kept the old FPM service running: switch the upstream socket back and reload Nginx/Apache. That’s minutes.
If you didn’t: reinstall packages, reconfigure, restart services—now it’s an incident, not a change.

6) Should I enable display_errors in production to see what’s happening?

No. Log errors instead. Displaying errors can leak secrets and break responses in ugly ways.
Use PHP-FPM logs, WordPress debug log (temporarily), and centralized logging. If you need visibility, add it safely.

7) Why do I get 502s only during traffic spikes after the upgrade?

Your new runtime may have different performance characteristics, or your caches reset and you’re seeing a cold-start penalty.
Under load, small slowdowns become queueing, then timeouts. Check FPM pool settings (pm.max_children), slowlog, and upstream timeouts.

8) Do I need to clear OPcache when switching PHP versions?

If you’re switching sockets between two FPM services, each has its own OPcache state—no cross-contamination.
If you are restarting or reloading the same service after code changes, resetting OPcache can prevent weird “old code still running” behavior.
But don’t blindly nuke caches across a fleet at once; that can cause a stampede.

9) What plugin types are the most dangerous during PHP upgrades?

Payment gateways, shipping/tax, page builders, security/firewall plugins, and anything bundling large vendor libraries.
They run deep in request paths and often have strong assumptions about types and server environment.

10) If I can’t do a canary, what’s the next best thing?

At minimum: run old and new PHP-FPM side-by-side on the same host and switch via reloadable config, with a tested rollback.
If you’re single-node, schedule a low-traffic window and accept that “no downtime” becomes “short downtime,” then reduce risk with exhaustive staging tests.

Conclusion: practical next steps

PHP incompatibility isn’t a moral failing. It’s a predictable outcome of running a large plugin ecosystem on a runtime that keeps improving its correctness.
Your job is to make the upgrade boring: inventory, stage, canary, switch, and rollback fast if needed.

  1. Today: Confirm your real web runtime (FPM version, socket wiring) and centralize the logs you’ll need in an incident.
  2. This week: Build a staging environment that mirrors production’s plugins, caching, and workflows. Test the admin and checkout paths, not just the homepage.
  3. Next change window: Run parallel PHP-FPM versions and canary one node. Watch error logs and tail latency. Roll forward only when you see clean signals.
  4. After rollout: Remove version skew, document rollback, and stop doing “two changes at once” upgrades. Your future self has enough hobbies.
← Previous
Big systems, small mistakes: why “one line” can cost a fortune
Next →
MySQL vs MariaDB Backup Wars on a VPS: mysqldump vs Physical Backups

Leave a comment