web-cp.net

"open source web hosting control panel"

← Back to Blog
PHP 8.4 in Shared Hosting Environments — What Actually Changed, What Breaks, and How to Run Multiple Versions Cleanly

PHP 8.4 in Shared Hosting Environments — What Actually Changed, What Breaks, and How to Run Multiple Versions Cleanly

2026-05-26 · Ethan Caldwell Techno Blog

PHP 8.4 was released in November 2024. That was eighteen months ago. On most shared hosting infrastructure running web-cp or any comparable panel, the conversation about 8.4 has been deferred — there is always something more immediately pressing, the tenants haven't asked for it, and the pain of supporting multiple PHP versions in a multi-tenant Apache environment is real enough that it is tempting to wait until 8.3 approaches its end-of-life window and the upgrade becomes mandatory.

The deference is understandable. It is also increasingly difficult to justify. PHP 8.4 is the first version in the 8.x series that makes changes significant enough to matter for the operational profile of a shared hosting environment — not because it breaks everything, but because it introduces deprecations that will affect a meaningful fraction of tenant codebases, changes the JIT configuration surface in ways that have real implications for multi-tenant resource management, and ships with a Fibers API that is now stable enough that tenant applications are beginning to use it in ways that your existing monitoring will not correctly attribute.

This article covers the practical questions: what actually changed in 8.4 that matters for shared hosting operators, how to run 8.4 alongside 8.1, 8.2, and 8.3 cleanly in an Apache environment, how web-cp's VirtualHost template system maps to a multi-version configuration, and which deprecations will hit tenant codebases first.

What Changed in 8.4 That Actually Matters for Shared Hosting

The PHP 8.x series has been incrementally strong. 8.0 brought union types and named arguments. 8.1 added enums and fibers. 8.2 added readonly classes and deprecated various inconsistencies. 8.3 brought typed class constants and the json_validate() function. Each version was a meaningful upgrade but none of them forced a conversation at the infrastructure level.

PHP 8.4 is different in three specific ways.

Property hooks. PHP 8.4 introduces property hooks — a get/set mechanism for class properties that is a significant language feature, not a syntactic convenience. Property hooks allow you to define get and set behaviour directly on property declarations, eliminating the boilerplate accessor pattern that PHP developers have been writing since PHP 5. This is the kind of language change that gets adopted quickly in modern frameworks, which means your tenants will start expecting it to be available within months of their frameworks upgrading.

Asymmetric visibility. You can now declare a property as public private(set) — publicly readable but only writable from within the class. This is a smaller but equally breaking-in-the-positive-sense change: code that was previously structured around read-only public properties via readonly or via accessor methods will start being written differently. It is a language-level change that has no backward compatibility problem but which means that PHP 8.4 code will increasingly look different from PHP 8.3 code in ways that matter for readability and review.

Deprecated implicit nullable types. This is the deprecation that will affect the most existing tenant code. In PHP 8.4, declaring a parameter type without explicitly marking it nullable — while providing a null default — is now deprecated. The pattern function foo(string $bar = null) was always technically imprecise, since it implied ?string without saying so. PHP 8.4 now emits E_DEPRECATED for this pattern. PHP 8.5 or 9.0 will remove it entirely. This affects a very large fraction of existing PHP codebases, including plugins and themes for WordPress, Joomla, and Drupal that have not been updated since the standard was introduced.

new in initialisers. PHP 8.4 allows new expressions in property initialisers, constant expressions, and function default values. This reduces the boilerplate around DI containers and service classes in ways that are immediately visible to framework developers. It is a minor-seeming addition with significant adoption surface because it unblocks patterns that PHP developers have been working around for years.

Lazy objects. PHP 8.4 introduces a native lazy object mechanism — the ability to declare that an object should only be fully constructed when actually accessed, rather than at the point of instantiation. This is a reflection-based feature that has been implemented in various userland ORM and DI frameworks with varying success and inconsistent performance profiles. The native implementation is both faster and more correct than any userland approach, and it will appear in the next generation of framework releases.

The operational consequence of these changes for shared hosting operators is not that everything breaks immediately — most existing code will run on 8.4 with deprecation notices rather than fatal errors. It is that 8.4 represents the point at which the gap between your current PHP offering and the language that modern PHP frameworks target begins to widen in ways that tenants will notice.

The Deprecation Map What Tenant Code Will Break

The implicit nullable types deprecation is the broadest. To quantify the scope: an analysis of the top 1,000 WordPress plugins by active install count shows that approximately 23% contain at least one instance of this pattern. For Drupal modules, the figure is closer to 31%. For Joomla extensions, the maintenance quality varies enough that a definitive number is harder to produce, but the pattern is endemic.

The practical consequence at the PHP 8.4 level is E_DEPRECATED notices, which in most production configurations will be logged but not displayed to end users and will not break functionality. The functional break comes when PHP 9.0 removes the implicit nullable behaviour entirely. The operational reason to care now is that tenants whose code emits deprecation notices in bulk are creating noise in their error logs that may mask genuine errors, and that the deprecation notices themselves are a forward signal of work that will eventually be forced.

The second significant deprecation is the removal of most POSIX functions from FILTER_SANITIZE_* handling, and the deprecation of round() with modes that were never correctly specified. Neither of these is as broadly impactful as the implicit nullable change but both appear with some frequency in financial calculation code and input sanitisation code that has not been reviewed recently.

The third category is the removal of the ability to call array_key_exists() on objects that implement ArrayAccess. This was already generating E_NOTICE in PHP 8.3; in 8.4 it is a deprecation that will become an error in the next major version. Code that uses array_key_exists() on SPL objects or on Doctrine collections will need to be updated.

The tooling situation for deprecation auditing is good. Running php -d error_reporting=E_ALL -d display_errors=1 against an application codebase and capturing the output is the manual approach. The phpstan and psalm static analysis tools, with their 8.4 platform targets, will identify most of the implicit nullable issues automatically. For shared hosting operators who want to give tenants a self-service deprecation check, it is worth documenting the rector tooling, which can automatically upgrade many of these patterns.

Running Multiple PHP Versions on Apache with mod_php and php-fpm

The architecture question for multi-version PHP in a shared hosting environment is whether you are using mod_php (the Apache module) or php-fpm (the FastCGI process manager). The answer determines everything about how per-domain version selection works.

If you are running mod_php, you are constrained to a single PHP version per Apache instance because mod_php loads the PHP interpreter as a module into the Apache process, and Apache only loads one version of any given module. Multi-version support with mod_php requires running multiple Apache instances on different ports or using mod_proxy to route requests to separate Apache instances — a configuration that is both operationally complex and resource-intensive. The practical recommendation for any shared hosting environment that needs to support multiple PHP versions is to move to php-fpm.

php-fpm is the correct architecture for multi-version shared hosting, and if you are not running it already, the migration is worth the effort even if the only driver is the multi-version requirement. With php-fpm:

  • Each PHP version runs as a separate pool of processes with its own configuration

  • Apache routes requests to the appropriate pool via mod_proxy_fcgi

  • Per-domain PHP version selection is a single ProxyPassMatch directive in the VirtualHost configuration

  • Resource isolation between tenants is substantially better than with mod_php because each pool can have independent process limits, memory limits, and execution time constraints

  • The security surface is smaller because the PHP process does not share address space with the Apache process

The installation approach varies by distribution. On Debian Trixie (the current recommendation for new deployments) with the packages.sury.org repository providing PHP packages:

bash

# Install multiple PHP versions
apt install php8.1-fpm php8.2-fpm php8.3-fpm php8.4-fpm

# Verify each service
systemctl status php8.1-fpm php8.2-fpm php8.3-fpm php8.4-fpm

# Each will listen on its own Unix socket by default:
# /run/php/php8.1-fpm.sock
# /run/php/php8.2-fpm.sock
# /run/php/php8.3-fpm.sock
# /run/php/php8.4-fpm.sock

The Apache configuration for routing to a specific PHP-FPM pool uses mod_proxy_fcgi. The per-VirtualHost configuration that selects the PHP version looks like this:

apache

<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/example.com/public_html

    # Route PHP requests to the 8.4 pool
    <FilesMatch "\.php$">
        SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost"
    </FilesMatch>

    # Alternatively, per-directory PHP version selection:
    <Directory /var/www/example.com/public_html/legacy-app>
        SetHandler "proxy:unix:/run/php/php8.1-fpm.sock|fcgi://localhost"
    </Directory>
</VirtualHost>

The ProxyPassMatch approach is an alternative that some operators prefer for legacy compatibility:

apache

ProxyPassMatch ^/(.*\.php)$ unix:/run/php/php8.4-fpm.sock|fcgi://localhost/var/www/example.com/public_html/$1

Both approaches work. The SetHandler approach is preferred in current Apache documentation and produces slightly cleaner request handling.

Configuring web-cp VirtualHost Templates for Per-Domain PHP Version Selection

Web-cp's VirtualHost template system exposes the variables that make per-domain PHP version selection maintainable at scale. The template approach means you do not need to hand-edit individual VirtualHost files for each domain; instead, the template generates the correct configuration from variables that can be set per-domain in the control panel.

The relevant template modification adds a PHP_VERSION variable that maps to the corresponding php-fpm socket. In the server-level VirtualHost template, the addition looks like this:

apache

# In your VirtualHost template (/etc/web-cp/templates/virtualhost.conf.tmpl or equivalent)
<VirtualHost *:80>
    ServerName {DOMAIN}
    ServerAlias www.{DOMAIN}
    DocumentRoot {DOCROOT}

    # PHP-FPM version selection
    # PHP_VERSION variable set per-domain in web-cp domain settings
    # Defaults to system default if not set
    <FilesMatch "\.php$">
        SetHandler "proxy:unix:/run/php/php{PHP_VERSION}-fpm.sock|fcgi://localhost"
    </FilesMatch>

    # Standard logging
    ErrorLog /var/log/apache2/{DOMAIN}-error.log
    CustomLog /var/log/apache2/{DOMAIN}-access.log combined
</VirtualHost>

The domain-level variable PHP_VERSION is set in the reseller or server control panel and propagates to the template when the VirtualHost is regenerated. The values are constrained to the installed php-fpm versions — 8.1, 8.2, 8.3, 8.4 — with a system-wide default fallback that protects against misconfiguration.

The operational workflow for adding PHP 8.4 support to an existing web-cp installation:

  1. Install php8.4-fpm and verify the socket path

  2. Add PHP_VERSION as a variable to the domain configuration schema

  3. Update the VirtualHost template to use the variable with a sensible default

  4. Regenerate VirtualHosts for the affected domains

  5. Reload Apache (systemctl reload apache2)

  6. Verify that PHP version selection is working correctly with a phpinfo() test file

The template approach has one important operational implication: regenerating VirtualHosts for all domains on a server applies the new template to all of them. If the default PHP_VERSION is 8.3 and a domain has been left without an explicit version setting, it will move to 8.3 on template regeneration regardless of what it was running before. The safest approach is to set explicit PHP_VERSION values for all domains before making template changes.

JIT in a Multi-Tenant Context: When It Helps and When It Hurts

PHP's JIT compiler was introduced in 8.0 and has matured significantly through the 8.x series. PHP 8.4 ships with JIT compilation that is meaningfully more effective than the 8.0 implementation for a broader range of code. The question for shared hosting operators is not whether JIT is good in principle — for CPU-bound workloads it is — but whether enabling it in a multi-tenant context is operationally sane.

The case for JIT in multi-tenant shared hosting is weak for most workloads. The majority of web application code in shared hosting environments is I/O bound: it waits for database queries, waits for network responses, and waits for file system operations. JIT provides its performance benefit by recompiling hot code paths into native machine instructions, which reduces CPU cycles for computation-heavy operations. If the bottleneck is not computation, JIT does not help.

The additional concern in multi-tenant contexts is memory. JIT requires a code region in the opcache memory buffer — typically 64 to 128 MB for a useful configuration — which is shared across all tenants whose requests hit the same php-fpm pool. In a high-tenant-count environment with heterogeneous workloads, the JIT code region can become a contention point.

The case for leaving JIT disabled in shared hosting pools is straightforward: you avoid the memory overhead, you avoid the debugging complexity that JIT adds to stack traces (though this has improved significantly in 8.4), and you lose essentially nothing because your workloads are not compute-bound.

The case for enabling JIT in specific isolated pools is more interesting. If you offer a dedicated php-fpm pool for specific tenants — a premium offering, or a tenant running a known compute-heavy application like a machine-learning inference service or a heavy image processing pipeline — enabling JIT for that pool specifically is a rational decision. The php-fpm pool configuration supports per-pool php.ini overrides that make this straightforward:

ini

; In /etc/php/8.4/fpm/pool.d/premium-tenant.conf
php_admin_value[opcache.jit] = tracing
php_admin_value[opcache.jit_buffer_size] = 128M

The default for standard shared hosting pools should be JIT disabled (opcache.jit = off). The documentation for this decision should be surfaced to tenants who ask — many WordPress developers have read that JIT is a significant PHP 8.x improvement and will ask why it is not enabled. The honest answer (their workload is I/O bound, not compute bound, and enabling JIT would not help them) is both true and reassuring.

Fibers and Async Code: Monitoring Implications You Need to Know

PHP Fibers were introduced in 8.1 and are now stable and widely used in the ReactPHP, Amp, and Revolt event loop ecosystems. PHP 8.4 does not change the Fibers API significantly, but it is the version at which Fiber-based async code is sufficiently mature that it will appear in a meaningful fraction of production tenant applications.

The monitoring implication is specific and frequently overlooked: traditional per-request PHP monitoring assumptions break for long-running Fiber-based applications.

A conventional PHP request handles one HTTP request, runs synchronous code from start to finish, and exits. The process lifecycle maps directly to the request lifecycle. Tools that report on PHP process utilisation — including web-cp's built-in resource monitoring, most server monitoring dashboards, and most APM tools — are built around this assumption.

A PHP application using event loops and Fibers may run as a long-lived process that handles thousands of requests before exiting. The process memory grows over its lifetime in ways that do not map to any single request. The CPU usage is bursty in ways that correspond to event loop cycles, not to individual request handling. A process that is correctly idle between requests looks, to conventional monitoring, like a process that is doing nothing and should perhaps be killed.

The practical consequence for shared hosting operators is that any tenant running a ReactPHP, Amp, or Revolt-based application under php-fpm needs a pool configuration that is aware of the long-running process model. The relevant php-fpm directives are:

ini

; For event-loop/Fiber-heavy applications
pm = static                    ; Static process management
pm.max_children = 4            ; Limited concurrency, each process handles many requests
pm.max_requests = 0            ; No automatic process recycling (or set to a high value)
request_terminate_timeout = 0  ; Do not kill based on request time alone

The monitoring implication for resource accounting in multi-tenant environments is that the standard metric of "memory per process" no longer meaningfully corresponds to "resource consumption per tenant" for these workloads. A Fiber-based application running correctly may consume significantly more memory per process than a conventional PHP application while serving more requests per unit of CPU than the monitoring suggests.

This is not a reason to prohibit Fiber-based applications in shared hosting — it is a reason to be deliberate about how you configure and monitor the pools that run them.

The Migration Conversation with Tenants

The operator's job when a new PHP version matters is not to force tenants to upgrade but to give them accurate information and a reasonable timeline. The communication framework for PHP 8.4 availability has three components.

First: announce availability with a compatibility guide. When PHP 8.4 becomes available as a selectable option in the control panel, the announcement should include a brief compatibility guide that describes the deprecated patterns that affect the most common CMS and framework versions. WordPress compatibility for 8.4 is good — the core team tests against it actively. WooCommerce is slightly behind but current. Plugins are the variable factor and the part of the tenant's stack they are least likely to have thought about.

Second: provide a test-before-switching workflow. The safest migration path for a tenant is to enable PHP 8.4 on a staging subdomain or in a development environment before switching their production domain. Web-cp's per-subdomain configuration supports this — a tenant can point staging.example.com to PHP 8.4 while example.com remains on 8.3. Document this workflow explicitly in the announcement.

Third: set a timeline for deprecating old versions. PHP 8.1 reached end-of-life in December 2024. Shared hosting infrastructure still running 8.1 for tenant compatibility is running an unsupported version. The practical timeline for a responsible operator is: announce PHP 8.1 end-of-hosting-support with a six-month window, during which time tenants are notified repeatedly that 8.1 will be removed and must migrate to 8.2 or later. The same process applies to 8.2 when its EOL arrives in December 2025.

The tenants who generate support tickets during PHP migrations are almost always running a specific incompatible plugin or extension. The most efficient response workflow is a documented compatibility checklist for the five most common issues in your tenant base, available to support staff as a first-response template. For most environments, those five issues cover 80% of migration tickets.

FAQ

Does PHP 8.4 work with WordPress? Yes. WordPress 6.4 and later have been tested against PHP 8.4. The WordPress core team actively tests against the current PHP version. Individual plugins may have compatibility issues, particularly older plugins that use the implicit nullable type pattern. The WordPress Plugin Repository's compatibility data is generally accurate for major plugins; for less-maintained plugins, testing on a staging environment before production migration is the correct approach.

Can I run PHP 8.1, 8.2, 8.3, and 8.4 simultaneously on the same server? Yes, with php-fpm. Each version runs as an independent set of processes with its own socket, configuration, and resource pool. The memory cost is the aggregate of all running pools, which makes it worth auditing which versions you actually need active. If no tenant is using 8.1, there is no reason to run the 8.1-fpm service.

Is the JIT compiler enabled by default in PHP 8.4? No. JIT is disabled by default in the PHP 8.4 php.ini. Enabling it requires setting opcache.jit to a mode value (typically tracing or function) and setting opcache.jit_buffer_size to a non-zero value. For shared hosting environments, the recommendation in this article is to leave JIT disabled in standard pools and enable it only in isolated pools for tenants with known compute-heavy workloads.

What is the difference between php-fpm and mod_php for shared hosting? mod_php runs the PHP interpreter as a module inside the Apache process. This means all PHP requests within an Apache instance use the same PHP version and the same user context as Apache. php-fpm runs PHP as a separate set of processes that Apache communicates with via FastCGI. This allows per-domain PHP version selection, per-pool resource limits, and improved security isolation. For shared hosting with multiple tenants, php-fpm is the correct architecture.

How do I check which PHP version a specific domain is using under web-cp? Place a file named phpinfo.php in the domain's document root with the contents <?php phpinfo(); ?>. Accessing the file via the domain's URL will display the PHP version information and the active configuration. Remove the file immediately after checking — phpinfo output is a security exposure in production. The alternative for checking without creating a temporary file is to use the CLI: php8.4 -r "echo PHP_VERSION;" for the system version, or to inspect the php-fpm pool configuration for the domain's VirtualHost.

What happens to a tenant's site if I remove a PHP version they are still using? The site stops working. Apache will attempt to proxy the PHP request to a socket that no longer exists and will return a 502 Bad Gateway error. The correct operational sequence is: confirm no active domains use the version being removed, then stop and disable the php-fpm service, then remove the package. Web-cp's domain configuration system should be queried before any PHP version removal to generate a list of domains still assigned to that version.

Should I upgrade all tenants to PHP 8.4 by default? No. The correct approach is to make PHP 8.4 available as a selectable option, communicate its availability and the migration guidance, and allow tenants to migrate at their own pace within a reasonable deadline. Forced migrations break sites and generate support tickets. Giving tenants control of the version selection — with clear guidance and a defined EOL timeline for older versions — is both less disruptive and more respectful of their operational reality.

PHP 8.4 is not a disruptive release in the way that 8.0 was. It does not break the PHP programming model or require framework-level rewrites. What it does is raise the technical floor of what current PHP looks like — the implicit nullable deprecation, property hooks, asymmetric visibility — and begin the cycle that will make 8.3 and below feel dated within the next eighteen months.

For shared hosting operators running web-cp, the work is well-defined. Install the php-fpm packages. Update the VirtualHost template system. Make PHP 8.4 selectable per domain. Communicate the deprecation landscape to tenants. Start the EOL conversation for PHP 8.1. None of this is urgent in the sense of being on fire. All of it is urgent in the sense that the gap between your infrastructure and current PHP development practice widens every month you wait.