Symfony OPcache Preloading: Production Performance Without Cold Starts
AI generated
SF
{ }
Symfony · OPcache · PHP-FPM · Deployment
Symfony OPcache Preloading
Production Performance Without Cold Starts

Without preloading, every single Symfony request recompiles the same thousand classes from OPcache bytecode all over again. With opcache.preload and a clean preload.php, the container, router and kernel classes stay permanently resident in every PHP-FPM worker before the first request even arrives.

18 min read opcache.preload · preload.php · PHP-FPM · Docker Symfony 7.x · PHP 8.3+

1. What OPcache preloading actually solves

Symfony OPcache preloading solves a problem most operators never see directly, because it hides inside every single request. Without preloading, PHP loads the compiled bytecode of a class from OPcache, but still has to check on every request whether the file changed, resolve the class tree, and relink inheritance relationships between interfaces, traits and base classes. In a Symfony project with several thousand classes from vendor bundles, generated container code and application services, this overhead adds up to measurable latency on every request.

Preloading fundamentally changes this model. A single script loads a defined list of classes fully into shared OPcache memory when each PHP-FPM worker starts, compiles them once, and links all inheritance relationships immediately. From then on, the complete class hierarchy stays permanently available for the lifetime of the worker, without any single request having to bear that cost again. For high-traffic Symfony applications, this is one of the few levers that saves real milliseconds per request without touching application code at all.

2. How preloading works under the hood

The PHP configuration directive opcache.preload points to a script that PHP executes exactly once when each worker starts. Inside that script you typically call opcache_compile_file() for every relevant file, or use require so PHP automatically compiles the referenced classes as well. Important for Symfony OPcache preloading: the preload script runs in a separate, isolated context with no access to the actual request, no superglobals and no database connection. Its only job is compiling classes and building reflection metadata, never executing business logic.

A second aspect concerns opcache.preload_user. Because the PHP-FPM master process often runs the preload script as root, PHP requires an explicit user for security reasons as soon as the script itself contains system calls. In practice the web server user, such as www-data, is usually enough. Without this directive PHP aborts the preload run with a clear error message as soon as root privileges are detected for the script execution.

3. Writing a preload script for a Symfony project

The most pragmatic entry point into Symfony OPcache preloading is not a handwritten script, but Symfony's own preload generator. The symfony/framework-bundle ships a class called ContainerPreloader that automatically determines every class referenced by the compiled container and generates a matching preload script. This file is typically produced during deployment with bin/console cache:warmup --env=prod and ends up under var/cache/prod/App_KernelProdContainer.preload.php.

Anyone who wants finer control over which classes go into preloading writes a custom script that includes the generated Symfony preload file and adds project-specific classes. That is especially useful when an application has hot paths that the standard container scan rarely captures, such as dynamically loaded value objects or DTOs from an API Platform integration.


<?php
// config/preload.php — custom Symfony preload entry point
declare(strict_types=1);

// Reuse Symfony's own generated preload file as the foundation.
// It already contains every class referenced by the compiled container.
$containerPreload = __DIR__ . '/../var/cache/prod/App_KernelProdContainer.preload.php';

if (file_exists($containerPreload)) {
    require $containerPreload;
}

// Add project-specific classes that the container scan misses,
// e.g. DTOs used only inside API Platform serializer groups.
$extraClasses = [
    __DIR__ . '/../src/Dto/OrderSummaryDto.php',
    __DIR__ . '/../src/Dto/CustomerAddressDto.php',
    __DIR__ . '/../src/ValueObject/Money.php',
];

foreach ($extraClasses as $file) {
    if (is_file($file)) {
        opcache_compile_file($file);
    }
}

4. Including the container, router and cache classes

The three most expensive classes during Symfony boot are almost always the compiled dependency injection container, the compiled router, and the compiled serializer metadata. Without preloading, these generated PHP files have to be parsed again on every request even though their content never changes between two deployments. Symfony OPcache preloading targets exactly these generated artifacts because they are particularly large and run through on practically every request.

The configuration in config/packages/prod/framework.yaml controls whether Symfony includes the preload process automatically. With framework.cache.prefix_seed and a custom compiler_pass that registers additional classes for the container compile, the scope can be extended deliberately. Important: value objects that are only instantiated via reflection, for example in Doctrine hydrators, should be added to the preload list manually, because the automatic container scan often misses them.


; php.ini — production OPcache preloading configuration
opcache.enable=1
opcache.enable_cli=0
opcache.preload=/var/www/app/config/preload.php
opcache.preload_user=www-data

; Preloaded classes stay in memory permanently — size the shared
; memory pool generously so it never gets evicted under pressure.
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.interned_strings_buffer=16
opcache.validate_timestamps=0

5. Enabling preloading in PHP-FPM and Docker

In containerized Symfony deployments, preloading is especially effective because every freshly started container brings a new PHP-FPM master process that runs the preload script exactly once on boot. The Dockerfile must ensure that cache:warmup --env=prod runs during the image build so the generated preload file is already present in the image instead of being created on the first request.

A common mistake: developers build the image without running the warmup step in the right order, after the Composer install and before the final stage copy. Then the preload file is missing from the production image, and opcache.preload points at nothing, without PHP throwing a visible error, because a missing preload file is silently ignored by PHP.


# Dockerfile excerpt — bake the preload file into the production image
FROM php:8.3-fpm AS build

WORKDIR /var/www/app
COPY . .
RUN composer install --no-dev --optimize-autoloader --classmap-authoritative

# Generate the compiled container, router and preload file at build time,
# not at first request time in production.
RUN php bin/console cache:clear --env=prod --no-debug \
    && php bin/console cache:warmup --env=prod --no-debug

FROM php:8.3-fpm AS runtime
COPY --from=build /var/www/app /var/www/app
COPY docker/opcache-preload.ini /usr/local/etc/php/conf.d/opcache-preload.ini

6. Understanding the mandatory restart on every deploy

The most important operational point about Symfony OPcache preloading: preloaded classes stay in memory until the worker process restarts. A plain opcache_reset() or cache:clear is not enough to activate new code while preloading is active, because preloaded classes are firmly anchored in shared memory and cannot be invalidated. Every deployment must therefore trigger a full restart of PHP-FPM, or, in Kubernetes, a rolling restart of the pods.

Anyone who overlooks this gets one of the most frustrating bugs in production: the code was deployed, tests are green, but the application keeps behaving like the old version, because preloaded classes from the previous deployment are still active. In Kubernetes it is therefore advisable to place the preload step in the container startup sequence and use the readiness probe to ensure a pod only receives traffic once the new preload run has completed.

7. Measuring preloading: benchmarks and real gains

The real impact of OPcache preloading cannot be seriously estimated, it must be measured. A simple but meaningful test: two identical PHP-FPM containers, one with opcache.preload enabled, one without, each hitting the same Symfony route with a load testing tool such as wrk or k6. In practice the biggest improvements show up on short, container-heavy requests with many autowired dependencies, and less on very compute-intensive endpoints whose runtime is dominated by database queries anyway.

As a rule of thumb: preloading typically saves a low single-digit to low double-digit millisecond range per request, depending on class count and inheritance depth of the project. For API-heavy Symfony applications with very short response times, this share often makes up ten to twenty percent of total latency, which is why the effort clearly pays off for high-load systems, while it barely registers for rarely used internal admin tools.


# Benchmark preload impact with wrk against a warm-cache Symfony endpoint
wrk -t4 -c100 -d30s --latency https://staging.example.com/api/orders/42

# Compare p50/p99 with opcache.preload=0 vs opcache.preload=/path/preload.php
# by toggling the ini directive and restarting php-fpm between runs
sudo systemctl restart php8.3-fpm

8. Pitfalls: ordering, fatal errors, autoloading

The most common mistake with Symfony OPcache preloading is the wrong order when loading classes with inheritance relationships. If a child class is loaded before its parent class is known, PHP aborts the entire preload run with a fatal error, and the whole worker starts with no preloading at all, without this being immediately obvious in production. That is why the preload script should never manually include files in arbitrary order, but consistently rely on Composer's autoloader order or the Symfony generated preload file, which already resolves these dependencies correctly.

A second pitfall concerns classes that check conditions at runtime, such as class_exists() checks for optional dependencies. Such classes must not be preloaded unconditionally if the checked dependency is missing from the production image, because PHP already throws an error while compiling. In practice it is advisable to run the preload script in staging with full logging enabled and take every warning seriously before rolling out to production.


<?php
// config/preload.php — defensive preloading with error isolation
declare(strict_types=1);

set_error_handler(static function (int $errno, string $errstr): bool {
    // Log but never let a single failing class abort the whole preload run
    error_log(sprintf('[preload] Skipped class due to: %s', $errstr));
    return true;
});

$files = require __DIR__ . '/preload-file-list.php'; // ordered by autoloader

foreach ($files as $file) {
    try {
        if (is_file($file)) {
            opcache_compile_file($file);
        }
    } catch (\Throwable $e) {
        error_log(sprintf('[preload] Failed compiling %s: %s', $file, $e->getMessage()));
    }
}

restore_error_handler();

9. Preloading in direct comparison

To make the decision for or against Symfony OPcache preloading tangible, a direct comparison of three realistic operating modes helps. Each mode has a clear trade-off between per-request boot overhead and operational deployment complexity.

Mode Class loading per request Deployment complexity Recommendation
No OPcache Full recompilation None Local development only
OPcache without preloading Bytecode cached, linking per request Low Solid default
OPcache with preloading Classes permanently linked in memory High, restart required per deploy High-load production systems
Preloading + validate_timestamps=0 Maximum, no filesystem checks Very high, strict deploy scripting required Container deployments with immutable images

In containerized Symfony environments with immutable images, the combination of preloading and opcache.validate_timestamps=0 is the most consistent stage, because an image is never changed while running anyway. In classic server setups with the occasional hotfix via FTP or rsync, this combination is risky, because changed files are simply ignored until an explicit restart happens.

Mironsoft

Symfony performance, deployment automation and PHP-FPM tuning

Symfony application with noticeable per-request boot overhead?

We analyze your container compile time, set up OPcache preloading ready for production, and integrate the warmup step cleanly into your deployment pipeline, including a rolling restart without downtime.

Performance audit

Boot time analysis and identification of the most expensive container classes

Preload setup

Production ready preload.php including error handling and logging

Deployment integration

Rolling restart, readiness probes and Docker image optimization

10. Summary

Symfony OPcache preloading removes one of the few remaining overhead shares that occur on every single request, even though the underlying code never changes between deployments. A clean preload script that builds on the Symfony generated container preload file and extends it with project-specific classes is the most pragmatic entry point. The configuration in opcache.preload and opcache.preload_user must be baked into the deployment image, not created at runtime.

The decisive operational point remains the restart requirement: preloading without a reliable rolling restart mechanism sooner or later leads to stale classes in production. Anyone who automates this process cleanly, bakes it into Docker images and validates it with load tests gains a measurable, lasting performance advantage per request for API-heavy Symfony applications, without touching the actual application code at all.

Symfony OPcache Preloading — the essentials at a glance

Configuration

opcache.preload and opcache.preload_user in php.ini, preload file generated by cache:warmup --env=prod.

Ordering

Load parent classes before child classes, otherwise the entire preload run aborts with a fatal error.

Deployment

Warmup at build time in the Docker image, never on the first request in production.

Operations

Every deploy needs a full worker restart, otherwise preloaded classes from the old deployment keep running.

11. FAQ: Symfony OPcache Preloading

1OPcache vs. preloading difference?
OPcache caches bytecode but resolves inheritance per request. Preloading links classes once, permanently, at worker start.
2Write my own preload script?
Not necessary. cache:warmup --env=prod already generates a suitable file. Custom script only useful for extra classes.
3Restart required after every deploy?
Yes. Preloaded classes stay in memory until the worker ends, cache:clear is not sufficient.
4What does opcache.preload_user do?
Sets the execution user, usually www-data, because root execution is blocked for security reasons.
5Worth it for small projects?
Barely. The effect scales with class count, with only a few hundred classes the gain is usually minimal.
6Error in the preload script?
Without error handling, aborts the entire run. An error handler with logging prevents this all-or-nothing behavior.
7Preloading in local development?
Unusual, since code changes constantly. Belongs exclusively in production and staging.
8How do I measure the impact?
Load test with wrk or k6, once with and without the preload directive, each after a fresh restart.
9Need to disable validate_timestamps?
No, independent directive. Amplifies the effect but requires disciplined deployment with a restart.
10Kubernetes vs. classic server?
Rolling deployments replace pods automatically with a fresh PHP-FPM process. Classic servers need a manual restart in the deploy script.