Skip to content
  • FG-62
  • 14 min read
  • Last checked November 8, 2026

How to Upgrade PHP Safely on a WordPress Site (and Roll Back if It Breaks)

Move a WordPress site to a newer PHP version without downtime: check support dates, scan plugins for incompatible code, test on staging, switch per site and roll back fast.

Run the health check

Site Health says “Your site is running on an outdated version of PHP”, WordPress shows “PHP Update Required”, or your host has announced that an old PHP version is being retired. You know you have to move. You also know that sites often go down with “There has been a critical error on this website” right after someone flips the PHP switch.

A PHP upgrade is not risky because PHP is fragile. It is risky because each new PHP version removes or tightens features that old plugin and theme code still relies on. This guide is for site owners, designers and freelancers who want to find the incompatible code before visitors do, switch one site at a time, and keep a rollback that takes under a minute.

What is happening

Your web server hands each page request to a PHP runtime (usually PHP-FPM or LiteSpeed’s LSAPI), and that runtime is one specific version, such as 8.2 or 8.4. Change the version and the same code runs on a different interpreter. Code that calls a removed function stops with a fatal error. Code that does something newly discouraged logs a deprecation notice and keeps working, for now.

PHP releases one new minor version each year, around late November. Each branch gets two years of active support (bug and security fixes) and then two more years of security fixes only, with the end dates rounded to 31 December. After that it gets nothing. Based on the PHP project’s published dates, this is where the branches stand in November 2026:

PHP branch Released Active support ends Security support ends Status now
7.4 and older 2019 or earlier Ended Ended (7.4 ended November 2022) End of life, move off now
8.0 November 2020 Ended Ended November 2023 End of life
8.1 November 2021 Ended 31 December 2025 End of life
8.2 December 2022 31 December 2024 31 December 2026 Security fixes only, ending in weeks
8.3 November 2023 31 December 2025 31 December 2027 Security fixes only
8.4 November 2024 31 December 2026 31 December 2028 Active support
8.5 November 2025 31 December 2027 31 December 2029 Active support

Check the “Supported Versions” page at php.net before you plan, because a new branch may have shipped since.

WordPress publishes its own compatibility list in the Core handbook. As of WordPress 7.0, the minimum PHP version is 7.4 (support for 7.2 and 7.3 was dropped), and WordPress 6.9 and later are listed as compatible with PHP 7.4 through 8.5. The WordPress.org requirements page recommends PHP 8.3 or newer. Core being compatible says nothing about your plugins and theme, and that gap is what this guide closes.

Before you start

You need your hosting panel (to change the PHP version), SSH or SFTP (to read logs and run scans), and ideally a staging copy on the same server. Take a full backup of files and database first, with your host’s tool or wp db export plus a copy of wp-content.

How to run the commands in this guide. Server commands run in an SSH terminal from your site folder, the one that contains wp-config.php (often public_html). WP-CLI is called wp on most servers; some hosts name it wp-cli, so swap the name if wp is not found. The PHPCompatibility scan needs PHP and Composer; run it on the server, or on your own computer against a downloaded wp-content (macOS and Linux via a package manager, Windows inside WSL).

One trap: WP-CLI uses the command line PHP binary, which is often a different version from the one serving the website. Compare the “PHP version” line of wp --info with Site Health.

Step 1: Record where you are and pick a target

Find the version the site actually runs

In wp-admin, go to Tools > Site Health > Info and open the Server section. Note the PHP version, the PHP SAPI (for example fpm-fcgi or litespeed), the memory limit and the max execution time. Save the whole report with the “Copy site info to clipboard” button for later comparison.

Ask your plugins what they claim to need

wp plugin list --fields=name,status,version,requires_php,tested_up_to,update
wp theme list --fields=name,status,version,update

This runs on the server over SSH. There are no placeholders. The --fields flag picks which columns to show: requires_php is the “Requires PHP” header the author declared, tested_up_to is the last WordPress version they tested, and update says whether a newer version is waiting.

Good output shows every plugin up to date with a recent tested_up_to. Warning signs are update showing available (update before you switch) and a tested_up_to several years old (possibly abandoned). The “Requires PHP” header is a minimum, not a maximum, so it cannot tell you a plugin is too old for PHP 8.4. Only a scan or a test can.

Choose the target version

Pick the newest branch that your host offers, that WordPress lists as compatible, and that has at least a year of security support left. In late 2026 that means 8.3 at the very least, and 8.4 for most sites. Jumping from 7.4 straight to 8.4 is fine, but you cross every breaking change at once.

Step 2: Scan the code for incompatibilities

PHPCompatibility is a free set of rules for PHP_CodeSniffer that reads your PHP files and flags functions, syntax and behavior that do not exist or have changed in the PHP versions you name. PHPCompatibilityWP is a WordPress flavor of it that knows about the polyfills WordPress core ships, so it does not flag functions core already provides.

Install the scanner

mkdir -p ~/phpcompat && cd ~/phpcompat
composer config --no-interaction allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/phpcompatibility-wp:"^3.0@dev"
vendor/bin/phpcs -i

Run these over SSH on the server, or in a terminal on your computer. The scanner lives in its own folder (~/phpcompat), outside your website, so nothing is installed into WordPress. The allow-plugins line lets Composer register the rule sets with PHP_CodeSniffer, and ^3.0@dev is the constraint the PHPCompatibilityWP project currently documents, since its PHP 8 rules ship as a development release.

Good output from the last command lists installed standards including PHPCompatibility and PHPCompatibilityWP. If you only see the built-in standards (such as PSR12 and PEAR), the installer plugin was not allowed to run; repeat the allow-plugins line and run composer install again.

Run the scan

vendor/bin/phpcs -p --standard=PHPCompatibilityWP --extensions=php --runtime-set testVersion TARGET_VERSION- --report=summary SITE_PATH/wp-content/plugins SITE_PATH/wp-content/themes SITE_PATH/wp-content/mu-plugins

Run this from the ~/phpcompat folder.

  • TARGET_VERSION is the PHP version you are moving to, such as 8.4. The trailing hyphen means “this version and everything after it”. If the site must keep working on the old version during the transition, use a range like CURRENT_VERSION-TARGET_VERSION, for example 8.1-8.4.
  • SITE_PATH is the full path to your WordPress folder, which you can print with pwd when you are in the folder containing wp-config.php.
  • -p shows progress, --extensions=php limits the scan to PHP files, and --report=summary prints a list of files with their error and warning counts instead of every line.

Good output is a short summary, or none, ending with no errors. Bad output lists files with ERROR counts. Re-run on one plugin folder without --report=summary to see each line number, its severity and a message such as “Function create_function() is deprecated since PHP 7.2 and removed since PHP 8.0”.

How to read the results

  • ERROR means the code uses something removed or changed in a breaking way in your target range. Treat each as a likely fatal error.
  • WARNING usually means a deprecation. The code still runs but logs a notice and will break in a later version.
  • False positives happen, usually in code that checks function_exists() or the PHP version first.
  • Static scanning cannot see everything. Code built at runtime, encoded files (ionCube) and behavior changes that depend on data (such as passing null to a string function) only show up when the code runs. Step 3 catches those.

Update any plugin or theme with errors, then re-scan. The Advanced section below lists the changes that cause most findings and how to fix your own code.

Step 3: Switch the staging copy and read the error log

Turn on logging without showing errors to visitors

Edit wp-config.php on the staging copy and make sure these lines sit above the line that says “That’s all, stop editing!”:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

This is a file you edit, not a command. There are no placeholders. WP_DEBUG turns on notices and deprecations, WP_DEBUG_LOG writes them to wp-content/debug.log, and the last two keep them off the screen. Our separate wp-config constants cheat sheet explains each constant in more depth.

Switch the PHP version for staging only

Change the version for the staging site in your panel (Step 5 shows where). Then clear every cache: the page cache plugin, any server cache, and the object cache with wp cache flush. PHP-FPM reloads with the new version, so OPcache starts empty on its own.

Exercise the site, then read the log

Click through what matters: home page, a post, a builder page, a test form submission, search, login, the editor (save a post) and, for a store, a test checkout. Then read the log:

tail -n 200 wp-content/debug.log
grep -c "PHP Deprecated" wp-content/debug.log
grep "PHP Fatal error" wp-content/debug.log | sort | uniq -c | sort -rn | head -20

Run these over SSH from the site folder. There are no placeholders. tail -n 200 shows the last 200 lines; the grep -c line counts deprecation notices; the last line groups identical fatal errors and shows the most frequent first. If your host keeps its own PHP log, find it in the panel’s Logs section or as an error_log file in the site folder.

A typical line looks like this:

[DD-Mon-YYYY HH:MM:SS UTC] PHP Fatal error:  Uncaught Error: Call to undefined function create_function() in /SITE_PATH/wp-content/plugins/PLUGIN_SLUG/includes/widget.php:LINE_NUMBER

Read it from right to left: the folder after plugins/ (here PLUGIN_SLUG) or themes/ names the culprit, the file and LINE_NUMBER tell its developer exactly where to look, DD-Mon-YYYY HH:MM:SS is the time of the error, and the message tells you which PHP change broke it. Good output is no fatal errors and only a few deprecations. Any fatal error means production waits.

Step 4: Check extensions, ini settings and encoded files

Each PHP version on a server is a separate installation with its own extensions and settings, so a switch can quietly change both. After switching staging, open Tools > Site Health > Info and compare the Server and Media Handling sections with the report you saved in Step 1. Watch for:

  • Missing extensions. WordPress wants curl, dom, exif, fileinfo, intl, mbstring, openssl, xml, zip, and imagick or gd. Without an image library, thumbnails stop generating; without intl or mbstring, some store and multilingual plugins fail.
  • Reset ini values. Panels usually store custom memory_limit, upload_max_filesize and max_execution_time values per PHP version, so they may be gone after the switch.
  • ionCube encoded plugins. Loader support for a new PHP branch can lag months behind its release. Confirm with the vendor that your target version is supported.

Step 5: Switch production in the panel

Most panels let you set the PHP version per site or per domain, so you can move one site without touching the others:

  • cPanel: Software > MultiPHP Manager, check the domain, choose the version and click Apply. On servers with CloudLinux you may see “Select PHP Version” instead, which also controls extensions and options for that version.
  • Plesk: Websites & Domains, then the domain’s PHP settings, where you choose the PHP version and handler.
  • Other panels: look for “PHP version”, “PHP settings” or “Runtime” on the website’s own settings page.

Do it at a quiet time with the error log open. Clear caches, repeat the Step 3 click-through on the live site and watch the log for an hour.

Leftover handler lines in .htaccess

On Apache servers, cPanel’s MultiPHP Manager records the version in .htaccess as a block like this:

# php -- BEGIN cPanel-generated handler, do not edit
<IfModule mime_module>
  AddHandler application/x-httpd-ea-phpNN .php .php8 .phtml
</IfModule>
# php -- END cPanel-generated handler, do not edit

This sits in the .htaccess file in your site folder. phpNN is the version without the dot, such as php84. If you migrated a site from another server, an old block like this can point at a version that does not exist on the new server, which shows up as a 500 error or as browsers downloading PHP files instead of running them. Let the panel rewrite the block by choosing the version again rather than editing it by hand. Our 500 Internal Server Error guide covers broken .htaccess files in more detail.

Step 6: Roll back fast if something breaks

If the live site shows a critical error after the switch, do not start debugging on production. Put it back first:

  1. Switch the PHP version back to the previous one in the same panel screen. This takes effect within seconds on most setups.
  2. Clear the page cache and any CDN cache so visitors stop getting a cached error page.
  3. Check the site loads, then copy the fatal error lines from the log (with timestamps) for the plugin developer or for your own fix list.

If wp-admin is still reachable but one plugin is broken, WordPress may have emailed the admin address a recovery mode link. If you cannot switch back straight away, deactivate the plugin named in the log instead:

wp plugin deactivate PLUGIN_SLUG --skip-plugins --skip-themes

Run this over SSH from the site folder. PLUGIN_SLUG is the folder name under wp-content/plugins that appears in the fatal error line. --skip-plugins and --skip-themes stop WP-CLI from loading the broken code while it works. Good output is “Success: Deactivated 1 of 1 plugins.” If WP-CLI crashes too, rename the folder over SFTP, for example to PLUGIN_SLUG.off. Our critical error guide covers recovery mode in detail.

A rollback is a pause, not a solution: set a date to fix the blocking plugin and try again.

Advanced: The PHP 8.0 to 8.4 changes that break plugins, and how to triage them

The changes behind most findings

Version Change What you see in the log
8.0 create_function() removed (deprecated since 7.2) Fatal: Call to undefined function create_function()
8.0 each() removed (deprecated since 7.2) Fatal: Call to undefined function each()
8.0 Curly brace string and array offsets like $str{0} removed Fatal: Array and string offset access syntax with curly braces is no longer supported
8.0 count() on something that is not an array or Countable throws instead of warning Fatal: count(): Argument #1 ($value) must be of type Countable|array, null given
8.0 Unparenthesized nested ternaries are a compile error Fatal parse error mentioning nested ternary expressions
8.1 Passing null to a non-nullable parameter of a built-in function deprecated Deprecated: Passing null to parameter #1 ($string) of type string is deprecated
8.1 Classes that extend built-in interfaces without return types Deprecated: Return type of … should either be compatible with … or the #[\ReturnTypeWillChange] attribute should be used
8.2 Dynamic properties (setting a property a class never declared) deprecated Deprecated: Creation of dynamic property Some_Class::$foo is deprecated
8.2 utf8_encode() and utf8_decode() deprecated; ${var} string interpolation deprecated Deprecated notices naming the function or the syntax
8.4 Implicitly nullable parameters (Type $x = null without ?Type) deprecated Deprecated: Implicitly marking parameter $x as nullable is deprecated

Fatal errors take the site or a feature down immediately. Deprecations do not, but a busy old plugin can write thousands per minute, and today’s deprecation is a future version’s fatal error.

Fixing them

Update the plugin or theme first; most active projects fixed these years ago. If an update does not exist, look for a maintained replacement. Only patch third-party code yourself as a stopgap, because the next update will overwrite it. For your own child theme or custom plugin, the fixes are mechanical: replace create_function() with an anonymous function, replace each() loops with foreach, declare properties at the top of the class (or add the #[\AllowDynamicProperties] attribute as a temporary measure), and write nullable parameters as ?Type $x = null.

Find who is making the noise

After a switch, group the log by plugin to see who is responsible:

grep -E "PHP (Deprecated|Warning|Fatal)" wp-content/debug.log | grep -oE "wp-content/(plugins|themes)/[^/]+" | sort | uniq -c | sort -rn

Run this over SSH from the site folder. The first grep keeps deprecation, warning and fatal lines; the second extracts just the plugin or theme folder; sort | uniq -c | sort -rn counts lines per folder with the noisiest first. Good output is a short list with small numbers. A plugin with tens of thousands of lines needs an update or a replacement even if nothing is visibly broken, because logging that much costs disk space and time on every request.

When you are done, set WP_DEBUG back to false on production. A public debug.log reveals file paths and plugin names, and it grows without limit.

When to contact your host

Open a ticket if the version you need is not offered, the switch does not take effect, an extension is missing, or the site breaks with no matching log entry. Send:

  • the domain and the exact PHP versions involved (from and to);
  • the UTC time you made the switch and the UTC time of the first error;
  • the relevant fatal error lines from the log, copied in full;
  • the Site Health “Server” section from before and after;
  • which extensions or ini values you expected and what you see instead.

Our diagnostic tools help you capture headers first, and if the site is down right now, start at the Emergency Room.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides
FG-132 Updates

How to Update WordPress Plugins, Themes and Core Safely

A calm, repeatable update routine for WordPress: what auto-updates and core rollback protection really do, what order to update in, what to test afterward and how to roll one plugin back.

Read the guide
11 min read

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close