Keeping classes permanently in memory instead of loading them on every request
OPcache Preloading loads selected classes and functions exactly once when a PHP-FPM worker starts, into a persistent memory area, so they are immediately available for every following request. Configured correctly it noticeably saves time for autoloading, inheritance resolution and interface checks, configured incorrectly it accomplishes nothing or produces hard to trace errors after every deploy.
Table of Contents
- 1. What OPcache Preloading actually solves
- 2. Basic configuration: enabling opcache.preload
- 3. Structuring a preload script correctly
- 4. Class order and inheritance resolution
- 5. Interaction with Composer autoloading
- 6. Preloading inside the deployment workflow
- 7. Common pitfalls in production
- 8. Monitoring and verifying preloading
- 9. Preloading in direct comparison
- 10. Summary
- 11. FAQ
1. What OPcache Preloading actually solves
OPcache itself already solves the problem that PHP files have to be reparsed and recompiled into opcodes on every request: the compiled bytecode lands in shared memory and is reused on the next request. OPcache Preloading goes one step further and solves a second, more subtle problem that pure opcode caching does not address: even when a class's bytecode is already in the cache, PHP still has to check on every request whether the class is instantiable, resolve its parent classes and interfaces, and relink the inheritance hierarchy.
This resolution disappears entirely once OPcache Preloading is enabled. When a PHP-FPM worker starts, exactly once per process rather than once per request, a preload script loads selected classes, fully links their inheritance hierarchy, and stores the result in a permanent memory area shared across the whole worker lifetime. Every following request in this worker already sees the class fully resolved, with no autoloading, no interface check, no re-linking of the parent class.
The effect is largest for applications with many classes and deep inheritance hierarchies, such as frameworks like Symfony or systems like Magento, which could potentially load hundreds to thousands of classes per request. For small scripts with only a few dozen classes, the effect of OPcache Preloading is barely measurable, because resolution costs per request are low anyway. Anyone introducing OPcache Preloading should therefore first check whether their own application is actually in the size range where the additional configuration effort pays off.
2. Basic configuration: enabling opcache.preload
The central configuration directive is opcache.preload, pointing to the absolute path of a PHP script that is executed exactly once when every PHP-FPM or CLI server process starts. Important: this directive only works when PHP runs in FPM or CLI server mode, not in classic CGI mode, and it must be set in the main php.ini or a loaded .ini file, not in an .htaccess or runtime configuration, because preloading must already be complete before any request arrives.
A second, often overlooked directive is opcache.preload_user. If PHP-FPM is started as root in order to run the worker processes under an unprivileged user afterward, the preload script must run under exactly that user, otherwise the start fails with an error message or preloading silently runs under the wrong file permissions. In most Docker based setups, where PHP-FPM already runs directly under a non root user, opcache.preload_user can be omitted.
; php.ini or /usr/local/etc/php/conf.d/opcache-preload.ini
; Path to the preload script, executed once per worker start
opcache.preload=/var/www/html/preload.php
; Only needed if php-fpm master process runs as root
; opcache.preload_user=www-data
; Preloading requires opcache to be enabled
opcache.enable=1
opcache.enable_cli=0
; Recommended baseline for production preloading setups
opcache.validate_timestamps=0
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
The combination with opcache.validate_timestamps=0 is worth noting. This directive disables the automatic check whether a source file has changed since it was last cached. Combined with OPcache Preloading this is nearly mandatory, because preloaded classes would not benefit from a source file change at runtime anyway, a PHP-FPM restart after every deploy being necessary regardless. Leaving validate_timestamps enabled risks inconsistent states between preloaded and regularly cached classes.
3. Structuring a preload script correctly
A preload script is, at its core, nothing more than an ordinary PHP file that runs when a worker starts. The central function is opcache_compile_file(), which compiles a file and stores its bytecode in preload memory without actually executing the file. For classes with inheritance chains, pure compilation is not enough though: for PHP to correctly resolve the parent class and interfaces, the class additionally has to be instantiated, or at least loaded via class_exists(), so the autoloader kicks in and the whole chain becomes known.
In practice, a robust preload script therefore consists of three parts. First the Composer autoloader is included, so class names can be resolved automatically. Then the most important classes of the application are loaded deliberately, usually via a list of directories searched with glob() or a RecursiveDirectoryIterator. At the end, the script logs how many classes were loaded successfully, so errors during worker start become visible instead of getting lost in the PHP-FPM logs.
declare(strict_types=1);
// preload.php — executed once per PHP-FPM worker start
require __DIR__ . '/vendor/autoload.php';
$preloadedCount = 0;
$failedClasses = [];
/**
* @param iterable<string> $classNames
*/
function preloadClasses(iterable $classNames, array &$failed): int
{
$count = 0;
foreach ($classNames as $className) {
try {
// class_exists triggers the autoloader and resolves the full
// inheritance chain, not just this one file
if (class_exists($className) || interface_exists($className) || trait_exists($className)) {
$count++;
}
} catch (\Throwable $e) {
$failed[] = $className . ': ' . $e->getMessage();
}
}
return $count;
}
// Only preload the hot path: value objects, core services, entities
$classMap = require __DIR__ . '/vendor/composer/autoload_classmap.php';
$hotPathPrefixes = ['App\\Domain\\', 'App\\Service\\', 'App\\Repository\\'];
$candidates = array_filter(
array_keys($classMap),
static fn (string $class): bool => array_any(
$hotPathPrefixes,
static fn (string $prefix) => str_starts_with($class, $prefix)
)
);
$preloadedCount = preloadClasses($candidates, $failedClasses);
if ($failedClasses !== []) {
error_log(sprintf('[preload] %d classes failed: %s', count($failedClasses), implode(', ', $failedClasses)));
}
error_log(sprintf('[preload] %d classes preloaded successfully', $preloadedCount));
The critical design mistake many first preload scripts make is trying to preload the entire codebase instead of limiting themselves to the actual hot path. That not only costs additional memory in the preload area, it also raises the risk that a rarely used class with missing dependencies brings down the entire worker start. A deliberate selection of the most frequently instantiated classes usually delivers the largest effect at a manageable risk.
4. Class order and inheritance resolution
One of the most underestimated rules of OPcache Preloading concerns the order in which classes are loaded. If a class inherits from a parent class that is not yet known, preloading that class fails and PHP logs a warning that the class could not be preloaded. Using class_exists() instead of a direct opcache_compile_file() call solves this problem automatically in most cases, because the autoloader loads the parent class on demand before the child class is resolved.
It becomes more critical with interfaces and traits shared by several classes. If an interface is already resolved during the preloading of a first class, it is already available for every following class in the same preload run and does not need to be resolved again. This reuse within the preload run itself is one reason why order matters less in practice than it initially appears, as long as class_exists() is used consistently instead of the direct compilation function.
A special case involves abstract classes and classes with untyped or generic constructor parameters that internally reference classes not yet loaded. Here it helps to first run the preload script with a small, deliberately chosen test set of classes and watch the error output in the PHP-FPM logs before enabling the full list. An iterative approach prevents a single faulty preload entry from blocking the entire worker start.
5. Interaction with Composer autoloading
OPcache Preloading and Composer autoloading complement each other but solve different problems at different points in time. Composer autoloading decides at runtime which file needs to be loaded for which class name, based on the PSR-4 mapping in autoload_psr4.php. OPcache Preloading uses this mapping exactly once at worker start to physically load the classes; after that the Composer autoloader no longer plays any role for preloaded classes, because they are already fully in memory.
A practical benefit of this combination: for preloaded classes, not only compilation is skipped on every request, but also the entire PSR-4 resolution path including the filesystem access that the Composer autoloader normally goes through to find the right file. For applications with thousands of classes and a correspondingly large autoload_classmap.php, skipping this lookup alone can be noticeable per request, especially when the realpath cache is undersized and frequent filesystem accesses would otherwise be required for path resolution.
Important to understand: preloaded classes are fixed for the entire lifecycle of the worker. An autoloader call for an already preloaded class returns true immediately, without loading the file again, because PHP internally recognizes that the class already exists as part of preload memory. For new classes that were not part of the preload script, the Composer autoloader continues to work as usual and loads them on demand normally.
6. Preloading inside the deployment workflow
OPcache Preloading changes the deployment workflow at one decisive point: because the preload script only runs once when a worker starts, a code deploy only becomes visible after a full PHP-FPM restart, a simple opcache_reset() or a graceful reload is no longer enough. Anyone who previously worked with systemctl reload php-fpm, because that sufficed for pure opcode caching, has to switch to an actual restart of the worker processes when using OPcache Preloading, for example systemctl restart php-fpm.
For deployment pipelines with multiple application servers this means: a rolling restart, where servers are restarted one after another while the load balancer redirects traffic to the remaining servers, becomes nearly mandatory. A simultaneous hard restart of all workers would lead to connection errors for the duration of preloading, because the new process can only accept requests once the preload run has completed. For a typical preload script with a few hundred classes, this process usually takes only a few hundred milliseconds, but with thousands of classes it can definitely be measured in seconds.
A second important point for the deployment workflow: the preload script itself must be part of the deployment artifact and updated in sync with the rest of the code. An outdated preload script referencing classes that have since been renamed or deleted produces errors on the next worker start, which depending on configuration can completely prevent the start. An automated smoke test right after the restart, checking whether PHP-FPM is actually accepting requests again, therefore belongs in every pipeline that uses OPcache Preloading in production.
7. Common pitfalls in production
The most common pitfall is the already mentioned omission of opcache.validate_timestamps=0. If this directive stays active, PHP keeps checking on every request whether regular, non preloaded files have changed, while preloaded classes remain unaffected. This leads to inconsistent behavior: changes to non preloaded files take effect immediately, changes to preloaded classes only after a restart, which leads to confusing debugging sessions in development when a fix apparently does not land.
A second common pitfall involves classes that evaluate environment variables or configuration values in the constructor or in static initializers. If such a class is instantiated during preloading to resolve its inheritance hierarchy, it can accidentally be initialized with the environment variables of the preload context instead of those of the actual request. It is therefore safer to load classes exclusively via class_exists() instead of actually instantiating them, because class_exists() only resolves the declaration without running the constructor.
declare(strict_types=1);
// WRONG: instantiating during preload can capture stale environment state
// $service = new PaymentGatewayClient($_ENV['GATEWAY_URL']);
// RIGHT: class_exists resolves the declaration and inheritance chain
// without running the constructor or any static initializer
if (!class_exists(PaymentGatewayClient::class)) {
error_log('[preload] PaymentGatewayClient could not be resolved');
}
// Verify how many classes actually made it into the preload memory
$preloadedClasses = array_filter(
get_declared_classes(),
static fn (string $class): bool => str_starts_with($class, 'App\\')
);
error_log(sprintf('[preload] verification: %d App\\ classes visible', count($preloadedClasses)));
A third, rarer but more consequential pitfall: preloaded classes permanently occupy the full configured memory area set via opcache.memory_consumption, regardless of how many requests actually use these classes. If the configured memory is undersized, OPcache can end up in a state where regularly cached files get evicted continuously, because the preload portion already occupies a large part of the available memory. A look at opcache_get_status() shows how much memory preloading actually claims.
8. Monitoring and verifying preloading
Whether OPcache Preloading is actually taking effect can be checked with the function opcache_get_status(), which under the key preload_statistics provides information about how many scripts, functions and classes were successfully preloaded. If this key is missing entirely, or shows a number close to zero even though the preload script should cover far more classes, the configuration is faulty, usually because opcache.preload points to the wrong path or the script throws a silent error at start.
For production operation, a simple health check endpoint is recommended that queries opcache_get_status() and checks the number of preloaded classes against an expected minimum. A monitoring system can query this endpoint regularly and raise an alarm when the number of preloaded classes unexpectedly drops after a deploy, which usually indicates a faulty preload script or a missed exception during worker start.
In addition, opcache_get_status() also provides memory statistics under memory_usage, split into used_memory, free_memory and wasted_memory. A continuously high value for wasted_memory after several deploys without a full PHP-FPM restart indicates that old class versions fixed by preloading are still occupying memory while new versions exist alongside them, a clear sign that the restart workflow in the pipeline is not working cleanly.
9. Preloading in direct comparison
A direct comparison shows how pure opcode caching and OPcache Preloading differ in the decisive properties.
| Property | Pure opcode caching | OPcache Preloading | Consequence |
|---|---|---|---|
| When loaded | On every first access per worker | Once, at worker start | No load cost per request |
| Resolving inheritance | On every request again | Once, permanently linked | No interface check per request |
| Deploy visibility | Visible after reload | Only after full restart | Rolling restart required |
| Memory footprint | Can be evicted | Permanently fixed | Plan memory size deliberately |
| Biggest effect for | Any codebase | Many classes, deep inheritance | Small scripts barely benefit |
The comparison makes clear that OPcache Preloading is not an addon you simply switch on, but an operational change with direct consequences for deployment and monitoring. Anyone who understands these consequences before enabling opcache.preload in production avoids the most common outages that typically occur right after the first activation.
10. Summary
Configuring OPcache Preloading in PHP 8 correctly means more than just setting opcache.preload in php.ini. A robust preload script deliberately loads the application's hot path via class_exists(), not via direct instantiation, to avoid side effects from constructors. opcache.validate_timestamps=0 is mandatory, because preloaded classes only get updated through a full restart anyway.
The deployment workflow has to switch to actual restarts instead of pure reloads, ideally as a rolling restart across multiple servers. Monitoring via opcache_get_status() reliably shows whether preloading is actually taking effect and how much memory is permanently reserved for it. Anyone who pays attention to these points gets a real, measurable performance gain from OPcache Preloading for applications with many classes, instead of a configuration line that stays ineffective in practice.
OPcache Preloading, the essentials at a glance
Set opcache.preload correctly
Path to the preload script in php.ini, only effective in FPM or CLI server mode, combined with opcache.validate_timestamps=0.
class_exists instead of instantiation
Resolves inheritance and interfaces without running constructors and without accidentally freezing environment state.
Deploy requires a restart
Preloaded classes only update on a full PHP-FPM restart, a rolling restart prevents downtime.
Monitoring via opcache_get_status
preload_statistics shows the actual number of preloaded classes, wasted_memory shows stale versions after a faulty restart.
11. FAQ: Configuring OPcache Preloading in PHP 8
1What is the difference between OPcache and OPcache Preloading?
2Where is opcache.preload configured?
3Why class_exists instead of instantiation?
4Is a reload after deploy enough?
5Must the entire codebase be preloaded?
6How do I check whether preloading is active?
7Does preloading work with Composer autoloading?
8What if the parent class is still unknown?
9Is preloading worth it for small applications?
10What should the deployment workflow look like?
Mironsoft
PHP performance tuning, OPcache configuration and deployment automation
Want OPcache Preloading introduced reliably in your setup?
We analyze your class structure, build a robust preload script and adapt the deployment workflow so rolling restarts run without downtime.
Preload script creation
Deliberate hot path selection, robust error handling and verification via opcache_get_status
OPcache tuning
Aligning memory configuration, validate_timestamps and interaction with the realpath cache
Deployment pipeline
Integrating rolling restarts, smoke tests and monitoring for preloading setups into CI/CD pipelines