OPcache preloading compiles PHP files into shared memory when the server starts, making those classes and functions available to every request without any autoloading or file reading overhead. For large legacy applications, preloading the right files can cut 5 to 15 percent off response times. Preloading the wrong files, or preloading them in the wrong order, can prevent PHP-FPM from starting at all.
This guide covers how to build a preload script that works reliably for large codebases, how to handle the dependency ordering problem, how to budget memory, and how to deploy preloading without risking downtime.
What Preloading Does
Normal OPcache caching works per-file, per-request. The first request that loads a file compiles it and stores the bytecode in shared memory. Subsequent requests read the cached bytecode directly. This is already fast, but there is still overhead: the autoloader has to resolve the class name to a file path, OPcache has to look up the file in its cache, and the cached bytecode has to be linked against the current request’s symbol table.
Preloading eliminates all of that. When PHP-FPM starts, it executes a preload script that loads specified files. Those files are compiled and their classes, functions, and constants are placed directly into the shared memory symbol table. Every subsequent request can use those symbols immediately, as if they were built into PHP itself.
The difference:
- Without preloading: Autoloader resolves class, OPcache looks up bytecode, bytecache is linked to request. Happens per-class, per-request.
- With preloading: Class is already in the symbol table. No autoloader call. No OPcache lookup. No linking. The class just exists.
For a class that is loaded on every request (your router, your service container, your request/response objects, your base controller), preloading removes all per-request loading overhead for that class.
How It Differs From Normal OPcache
Normal OPcache and preloading are separate mechanisms that complement each other.
Normal OPcache caches compiled bytecode per file. The cache is populated lazily as requests trigger file loads. Each cached file retains its identity as a separate compilation unit, and class inheritance relationships are resolved at load time within each request.
Preloading resolves everything at server start. Inheritance hierarchies are linked once. Interface implementations are verified once. The compiled classes in shared memory are fully resolved, which means there is no per-request cost for class linking.
This is why preloading helps most with applications that have deep inheritance hierarchies and heavy use of interfaces. A Laminas MVC application with abstract controllers, service manager factories, event listener interfaces, and hydrator base classes benefits disproportionately because preloading eliminates the repeated resolution of those inheritance chains.
Normal OPcache also benefits from preloading indirectly. Preloaded files do not occupy slots in the OPcache file cache, which frees capacity for other files. If your opcache.max_accelerated_files limit was tight, preloading your most common files reduces pressure on that limit.
Writing a Preload Script
The preload script is a regular PHP file specified in php.ini:
1 | opcache.preload=/var/www/myapp/preload.php |
PHP-FPM executes this file once at startup. The simplest approach is to require the files you want to preload:
1 |
|
This works but is fragile. Maintaining a manual list of hundreds of files is error-prone, and the ordering matters critically.
A Better Approach: Classmap-Based Preloading
If you are using Composer’s optimised classmap (and you should be in production, as covered in the Composer Autoloader Optimisation for Production guide), you can use the classmap to drive preloading:
1 |
|
This approach is maintainable because you control preloading at the namespace level rather than the individual file level. Adding a new namespace prefix is a one-line change.
The Dependency Ordering Problem
This is where preloading fails catastrophically in large applications.
When PHP preloads a file, it compiles the class definition. If that class extends another class or implements an interface, the parent class or interface must already be loaded. If it is not, the preload fails with a fatal error, and PHP-FPM does not start.
Consider this hierarchy:
1 | // AbstractController implements DispatchableInterface |
If your preload script loads AbstractController before DispatchableInterface, or loads DispatchableInterface before EventManagerAwareInterface, PHP cannot resolve the inheritance chain and the preload fails.
The require_once approach with the classmap iteration is particularly dangerous because the classmap is ordered alphabetically by class name, not by dependency order. AbstractController appears before EventManagerAwareInterface alphabetically, and the preload crashes.
Solving Dependency Ordering
There are three strategies:
Strategy 1: Use opcache_compile_file instead of require_once.
1 | foreach ($classmap as $class => $file) { |
opcache_compile_file compiles the file into OPcache without executing it or resolving class definitions. This avoids the dependency ordering problem entirely, but it also loses the main benefit of preloading: classes compiled this way are cached as bytecode but are not linked into the shared symbol table. They still need per-request linking.
This is a safe fallback that gives you some of the preloading benefit (no disk I/O) without the full benefit (no per-request class resolution).
Strategy 2: Sort by dependencies.
Write a script that analyses your class hierarchy and produces a topologically sorted list:
1 |
|
Your preload script then reads the sorted list and loads files in dependency order. This is robust but adds a build step.
Strategy 3: Use try/catch and multiple passes.
1 | $remaining = $filesToPreload; |
Each pass loads the classes whose dependencies were resolved in previous passes. After a few passes, most dependency chains are resolved. This is pragmatic and handles most real-world hierarchies in 2 to 3 passes.
Memory Budgeting
Preloaded classes consume shared memory permanently. They are loaded at server start and stay in memory until the server restarts. You need to budget this memory against your OPcache allocation.
Check your current OPcache memory usage:
1 | $status = opcache_get_status(); |
For a large Laminas application, preloading the framework core plus your most used application classes typically uses 10 to 30 MB of shared memory. Ensure your opcache.memory_consumption has headroom for this. If your current setting is 128 MB and you are using 110 MB without preloading, you need to increase it before enabling preloading.
A reasonable starting configuration:
1 | opcache.memory_consumption=256 |
What to Preload vs What to Skip
Preload classes that are loaded on every or nearly every request:
- PSR interfaces (ServerRequestInterface, ResponseInterface, ContainerInterface)
- Framework bootstrap classes (Application, ServiceManager, EventManager, Router)
- Your base controller or handler class
- Authentication and authorisation services
- Logger, configuration, and database connection wrappers
- Common value objects and DTOs used across the application
Skip classes that are loaded only on specific request paths:
- Admin-only controllers and services
- CLI command classes
- Migration classes
- Test utilities and fixtures
- Rarely used vendor package classes
- Classes that define constants from environment variables (these evaluate at preload time, which is server start, not request time)
A good heuristic: if a class loads on more than 50 percent of your production requests, preload it. If it loads on less than 10 percent, skip it. Between 10 and 50 percent, include it if it is part of a class hierarchy where the parent is already preloaded.
The Restart Requirement
Preloaded classes are loaded at server start. There is no way to update them without restarting PHP-FPM. This is the fundamental deployment complication.
With normal OPcache, you can clear the cache via opcache_reset(), a web endpoint, or a cachetool command. The next request recompiles from the updated files. With preloading, the classes in shared memory persist until the PHP-FPM master process restarts.
This means every deployment that changes a preloaded file requires a PHP-FPM restart. A graceful restart (kill -USR2) is the minimum. The master process spawns new workers with the new preloaded classes while the old workers finish their current requests.
Safe Deployment Patterns
Graceful restart with a symlink swap:
1 | # Deploy new release to a fresh directory |
The symlink swap ensures new requests hit the new code. The graceful restart ensures preloaded classes are updated. Old workers finish their requests with old preloaded classes, which is safe as long as the old code is still on disk (do not delete the old release directory immediately).
Canary deployment:
If your infrastructure supports it, deploy to a canary instance first. The canary restarts PHP-FPM, loads the new preloaded classes, and serves a fraction of traffic. If the preload script fails (fatal error, memory exceeded), only the canary is affected. The remaining instances continue serving on the old release.
This is the safest pattern for large applications where a preload failure would cause a full outage. A preload failure prevents PHP-FPM from starting, which means the server cannot serve any requests. A canary catches this before it affects the fleet.
Health check after restart:
1 | kill -USR2 $(cat /run/php-fpm.pid) |
Always verify the server is healthy after a restart when preloading is enabled. A missing interface in the preload chain turns a routine deployment into a complete outage.
Measuring Impact
Before enabling preloading, establish baseline metrics. After enabling, compare:
- Autoloader time per request. Preloaded classes never hit the autoloader. If you preload your 100 most common classes, autoloader calls per request drop by 100.
- OPcache hit rate. Should increase because preloaded files do not compete for OPcache slots.
- Memory usage per worker. Each PHP-FPM worker shares the preloaded memory. Per-worker memory may decrease slightly because classes are loaded once in shared memory rather than once per worker.
- Response time under load. The benefit of preloading scales with concurrency. Under low load, the per-request saving is small (a few milliseconds). Under high load, eliminating filesystem contention from class loading produces disproportionate gains.
The PHP Performance Playbook covers the full measurement methodology, including how to isolate preloading’s contribution from other performance factors. The Performance Optimisation for Zend Framework Applications chapter provides the framework-level context for where preloading fits in the overall performance strategy.
The Interaction With Composer’s Classmap
Preloading and Composer’s classmap authoritative mode are complementary, but they interact in a way that matters for deployment ordering.
Composer’s classmap authoritative mode means the autoloader only checks the classmap. If a class is not in the map, it is not found. Preloading means certain classes are already in the symbol table and the autoloader is never called for them.
The deployment sequence matters:
- Run
composer install --no-dev - Generate any build-time classes (Doctrine proxies, compiled templates)
- Run
composer dump-autoload --classmap-authoritative - Restart PHP-FPM (which executes the preload script)
If you restart PHP-FPM before generating the classmap, the preload script may fail if it relies on the classmap to find files. If you generate the classmap after restarting, the old classmap is in effect until the next restart.
The Composer Autoloader Optimisation for Production guide covers the classmap side of this in detail, including the patterns that break under authoritative mode.
When Preloading Is Not Worth It
Preloading adds deployment complexity. It is not always justified.
If your application is small (fewer than 100 classes loaded per request), the preloading benefit is a few milliseconds at most. The deployment complexity is not worth it.
If your deployment pipeline cannot handle PHP-FPM restarts gracefully (shared hosting, platforms without process management), preloading introduces too much risk. A failed preload means no PHP service at all.
If your application relies heavily on dynamic class loading that changes between deployments, maintaining the preload list becomes another thing that can go wrong during deploys.
For large legacy applications with hundreds of classes per request, deep inheritance hierarchies, and a deployment pipeline that already handles PHP-FPM restarts, preloading is one of the best performance optimisations available. It costs no application code changes, it requires no refactoring, and it directly targets the overhead that is unique to large PHP applications: the per-request cost of loading and linking hundreds of class definitions.