Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

PHP OpCache Configuration for Production

By Kokil Thapa | Last reviewed: September 2026

PHP OpCache configuration for production is the difference between a server that compiles every request and one that serves precompiled bytecode from shared memory. On real client projects running Laravel 12 or Laravel 13 on Ubuntu with PHP-FPM, I treat OpCache as mandatory infrastructure — not an optional speed tweak. This guide covers the ini settings I actually deploy, how they interact with PHP-FPM pool configuration, and the deploy-time gotchas that leave stale code running after a release.

What Does PHP OpCache Do in a Production Stack?

PHP is an interpreted language. Without OpCache, the Zend engine tokenises, parses, and compiles every .php file on every request. That work is pure CPU overhead. OpCache stores the compiled opcode in shared memory so subsequent requests reuse it.

The performance gain is immediate. A typical Laravel application loads hundreds of files per request. OpCache can cut that compile phase to near zero. On a production Laravel application I maintain, OpCache plus sensible Redis caching is the baseline before touching CDN or database tuning.

PHP OpCache Request FlowBrowserHTTP requestNginxor ApachePHP-FPMworker poolOpCacheshared memorycompiled bytecodeLaravel / Symfony Appcontrollers, models, vendor/cache hit
PHP OpCache configuration for production: compiled scripts live in shared memory between PHP-FPM workers

OpCache is bundled with PHP 8.3, 8.4, and 8.5. You do not install it separately on modern builds. Confirm it is active before tuning anything else.

php -v
php -m | grep -i opcache
php -i | grep "opcache.enable"

If opcache.enable shows Off, your production server is leaving performance on the table. Fix that before scaling hardware or rewriting queries.

Which OpCache Settings Should You Use in Production?

OpCache settings live in a dedicated ini fragment. On Ubuntu with PHP 8.5, the file is typically /etc/php/8.5/fpm/conf.d/10-opcache.ini. CLI and FPM use separate configs — always tune the FPM config for web traffic.

Below is a production baseline I use on Laravel and Symfony deployments. Adjust memory based on your codebase size.

; /etc/php/8.5/fpm/conf.d/10-opcache.ini

opcache.enable=1
opcache.enable_cli=0

; Memory — increase if opcache_get_status shows near-full usage
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000

; Production: do not re-stat files on every request
opcache.validate_timestamps=0
opcache.revalidate_freq=0

; JIT (PHP 8.x) — useful for CPU-heavy workloads
opcache.jit=1255
opcache.jit_buffer_size=128M

; Prevent stale includes in long-lived workers
opcache.max_wasted_percentage=10

; Preloading (optional, powerful for large apps)
; opcache.preload=/var/www/app/preload.php
; opcache.preload_user=www-data

Memory sizing

opcache.memory_consumption sets shared memory in megabytes. A small WordPress site may need 64 MB. A Laravel 13 app with a large vendor/ tree often needs 192–512 MB. Check usage with opcache_get_status() or a one-off script.

<?php
$status = opcache_get_status(false);
echo "Used: " . round($status['memory_usage']['used_memory'] / 1048576, 1) . " MB\n";
echo "Free: " . round($status['memory_usage']['free_memory'] / 1048576, 1) . " MB\n";
echo "Cached scripts: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";

If used memory stays above 90%, raise opcache.memory_consumption. Evicted scripts mean cache churn and lost performance.

File count limits

opcache.max_accelerated_files must exceed your total PHP file count. Count them on the server:

find /var/www/myapp -name "*.php" | wc -l

Set the limit to at least 1.5× that number. Laravel projects with many packages can exceed 10,000 files easily. The default of 10,000 is too low for most modern frameworks.

validate_timestamps — the production trade-off

When opcache.validate_timestamps=1, PHP checks file modification times and recompiles changed files. That is convenient in development. In production it adds filesystem stat calls on every request.

Set opcache.validate_timestamps=0 in production. PHP will never auto-detect file changes. You must reload PHP-FPM after every deploy. That is the correct trade-off for stable production traffic.

SettingDevelopmentProductionWhy
opcache.enable11Always on for realistic perf testing
opcache.validate_timestamps10Auto-reload in dev; manual FPM reload in prod
opcache.memory_consumption128256–512Large vendor trees need headroom
opcache.max_accelerated_files1000020000+Framework + packages exceed defaults
opcache.jitoff or tracing1255CPU-bound code benefits; I/O-bound less so
opcache.preloadoptionalrecommendedLoads core classes once at FPM start

Official reference for every directive lives in the PHP OpCache configuration documentation. Bookmark it when auditing a server someone else configured.

How Do You Invalidate OpCache After a Deployment?

This is where most production incidents start. You deploy new code. Users still hit old logic. OpCache cached the previous bytecode and validate_timestamps=0 prevents automatic refresh.

I've seen this on sister sites sharing a Deployer 7 pipeline. The symlink swaps correctly. PHP-FPM never reloads. The site runs yesterday's code until someone notices.

Deploy + OpCache Invalidation1. Git Pullnew release2. Symlinkcurrent swap3. FPM Reloadrequired step4. Livefresh cacheCommon Failure: Skip Step 3Symlink points to new codeOpCache still serves old bytecodeUsers see stale routes, configs, and bug fixes
Production OpCache invalidation requires a PHP-FPM reload after every code deployment

Reload PHP-FPM after deploy

Add this to your deployment script. With Deployer 7, a typical task looks like this:

task('php:fpm:reload', function () {
    run('sudo systemctl reload php8.5-fpm');
});

after('deploy:symlink', 'php:fpm:reload');

Use reload, not restart, for zero-downtime on a correctly configured pool. A full restart drops active connections. Reload gracefully replaces workers.

On Apache with mod_php, restart Apache instead. Most production Laravel stacks use PHP-FPM behind Nginx or Apache as a reverse proxy. See LEMP stack setup for the full wiring.

Alternative invalidation methods

These exist but I prefer FPM reload because they are reliable and simple.

  1. opcache_reset() — Clears the entire cache for that FPM pool. Requires a web-accessible script or artisan command. Risky if exposed publicly.
  2. opcache_invalidate($file, true) — Targets one file. Impractical for full deploys with thousands of changed files.
  3. Temporarily enable validate_timestamps — Works but adds stat overhead. Not a production pattern.

For Laravel, an Artisan command that calls opcache_reset() behind authentication is a useful emergency tool. Do not rely on it as your primary deploy step.

Should You Enable OpCache Preloading for Laravel or Symfony?

Preloading is the most underused OpCache feature in PHP 8.x. At FPM startup, PHP executes a preload script that loads specified classes into OpCache permanently. Every worker inherits them without lazy compilation on first request.

The gain is measurable on cold requests. First-page loads after worker spawn drop noticeably. For high-traffic sites, that matters during deploys and traffic spikes.

Laravel preload.php example

Laravel 12 and Laravel 13 ship with a stub at preload.php in the project root. Enable it in your OpCache ini:

opcache.preload=/var/www/myapp/current/preload.php
opcache.preload_user=www-data

The preload script typically loads framework core classes:

<?php
// preload.php
require __DIR__ . '/vendor/autoload.php';

$app = require __DIR__ . '/bootstrap/app.php';
$app->boot();

// Preload commonly used classes
$classes = [
    Illuminate\Foundation\Application::class,
    Illuminate\Http\Request::class,
    Illuminate\Http\Response::class,
    Illuminate\Routing\Router::class,
    Illuminate\Database\Eloquent\Model::class,
];

foreach ($classes as $class) {
    if (class_exists($class) || interface_exists($class)) {
        opcache_compile_file((new ReflectionClass($class))->getFileName());
    }
}

Preloading requires care. A syntax error in the preload script prevents FPM from starting. Test on staging first. Your staging environment should mirror production OpCache settings — see staging environment setup for the checklist.

On a legal-tech portal I built with Laravel, preloading plus OpCache cut cold-start latency on document upload routes. The win was modest but consistent. Combined with speed optimization work on assets and queries, it added up.

How Do You Tune OpCache JIT for PHP 8.5 Workloads?

PHP 8.x introduced JIT compilation alongside OpCache. JIT translates hot opcode paths into native CPU instructions. It helps CPU-bound code. It does little for typical CRUD apps that wait on MySQL or Redis.

The recommended production setting is opcache.jit=1255 with a buffer of 64–128 MB. The four-digit mode controls when and how aggressively JIT compiles.

OpCache JIT Tuning DecisionWhat is your bottleneck?CPU-boundmath, parsing, cryptoI/O-boundDB, API, file readsEnable JIT 1255128M jit_buffer_sizeJIT optionalfocus on queries + cache
PHP 8.5 OpCache JIT helps CPU-bound workloads; typical Laravel CRUD apps gain more from query and cache tuning

Measure before chasing JIT gains. Use load testing with k6 to compare latency with JIT on and off. If p95 response time does not move, your bottleneck is elsewhere.

Most eCommerce and CMS workloads — WooCommerce 11.1, Magento 2.4.x, custom Laravel carts — are I/O-bound. OpCache itself delivers the big win. JIT is icing.

PHP-FPM interaction

OpCache is shared across workers in a pool. JIT buffer is part of that shared segment. Combined with proper PHP-FPM pool tuning, you get consistent worker behaviour under load.

Key FPM settings that pair with OpCache:

  • pm.max_children — More workers share the same OpCache segment; memory is efficient.
  • pm.max_requests — Recycling workers clears per-worker state but OpCache persists in shared memory.
  • request_terminate_timeout — Long requests do not corrupt OpCache; they just hold a worker.

What OpCache Mistakes Break Production PHP Apps?

After years of debugging deploy and performance issues, these patterns repeat across client servers in Nepal and abroad.

Stale code after deploy

Symptom: fix is live on disk but behaviour unchanged. Cause: no FPM reload with validate_timestamps=0. Fix: add reload to CI/CD. Verify with a unique string in a response header or footer during deploy testing.

OpCache disabled on FPM but enabled on CLI

Running php artisan commands shows OpCache active. Web requests do not use it. Cause: separate ini files. Fix: edit /etc/php/8.5/fpm/conf.d/10-opcache.ini, not the CLI version.

Memory too small — silent evictions

Performance degrades gradually as the codebase grows. New vendor packages push cached scripts out. Monitor opcache_statistics.cache_full and oom_restarts in status output.

If opcache.preload points to an absolute path inside a release directory, it breaks on the next deploy. Point it at the stable current symlink path instead.

Running multiple PHP versions

Servers with PHP 8.3 and 8.5 side-by-side need separate OpCache configs per version. A common mistake on shared hosting and VPS setups. Each FPM service has its own shared memory segment.

Before vs After OpCache TuningBeforeCPU: 70–90%Parse every request500+ files compiledStale code on deployp95: 800ms+AfterCPU: 20–40%Bytecode from RAM20k files cachedFPM reload on deployp95: 200–400mstune
Proper PHP OpCache configuration for production reduces CPU compile overhead and stabilises response times after deploys

For ongoing monitoring, add OpCache metrics to your server checklist. A monthly review under support and maintenance catches memory creep before it becomes user-facing slowness.

Monitoring checklist

Run this after deploy and during performance audits:

  1. Confirm opcache.enable=1 in FPM phpinfo output.
  2. Check memory usage stays below 85% of allocated.
  3. Verify num_cached_scripts matches expected file count.
  4. Confirm FPM reload ran in deploy logs.
  5. Watch for oom_restarts incrementing — signals undersized memory.

On projects like Nepal Gift Card and Adventure Third Pole Trek, OpCache is step one in any performance review. Step two is database indexing. Step three is application caching. Skipping step one wastes effort on steps two and three.

Key Takeaways

  • Enable OpCache in the FPM ini file — not just CLI — with at least 256 MB for Laravel-sized codebases.
  • Set opcache.validate_timestamps=0 in production and reload PHP-FPM after every deploy.
  • Raise opcache.max_accelerated_files above your actual PHP file count to prevent silent evictions.
  • Add preloading for large PHP 8.5 apps; test the preload script on staging before enabling in production.
  • Pair OpCache with PHP-FPM reload in your Deployer or GitLab CI pipeline — never deploy code without invalidating cache.
  • Monitor opcache_get_status() monthly; memory pressure grows as packages accumulate.

People Also Ask

Is OpCache enabled by default in PHP 8.5?

OpCache is compiled into official PHP 8.5 builds but may ship disabled in distribution packages. Ubuntu and Debian often enable it in a conf.d fragment. Always verify with php -i | grep opcache.enable on the FPM SAPI, not just CLI.

Does OpCache work with Docker and container deployments?

Yes. Mount your OpCache ini as a config volume or bake it into the image. Reload the container's PHP-FPM process after code updates. In multi-stage Docker builds for Laravel, OpCache config belongs in the runtime stage alongside FPM pool settings.

How much memory should OpCache use for WordPress or WooCommerce?

WordPress 7.1 with WooCommerce 11.1 and a moderate plugin set typically needs 128–192 MB. Heavy plugin stacks or page builders may need 256 MB. Check cached script count and memory usage after importing a production database clone.

Can OpCache cause security issues?

OpCache itself is not a security vulnerability. The risk is operational: stale cached code after deploy can leave patched files inactive until FPM reload. Never expose opcache_reset() endpoints publicly. Treat FPM reload as a required deploy step, not optional.

Ship Faster PHP With Correct OpCache Settings

PHP OpCache configuration for production is not exotic tuning. It is baseline infrastructure for any PHP 8.3+ application serving real traffic. Set the ini values, disable timestamp validation, reload FPM on deploy, and monitor memory usage as your codebase grows.

If your Laravel or WordPress site still feels slow after OpCache is correct, the bottleneck has moved to queries, caching, or front-end assets. That is a different problem with different fixes. Start with OpCache — it takes thirty minutes and pays back on every request.

Need help auditing a production server or fixing deploy pipelines that skip FPM reload? Review the Linux system administration and testing and optimization services, or contact us for a server review. For related reading, see Ubuntu server setup for PHP apps, Dockerizing Laravel for production, and PostgreSQL vs MySQL for production. Use the JSON formatter when debugging API responses during performance work.

Frequently Asked Questions

OpCache stores compiled bytecode in shared memory so PHP skips re-tokenising, parsing, and compiling scripts on every request. Without it, that work is pure CPU overhead on every hit.

OpCache is compiled into PHP 8.5 builds but may ship disabled in distribution packages. Always verify with php -i | grep opcache.enable on the FPM SAPI, not CLI.

Edit the FPM fragment, typically /etc/php/8.5/fpm/conf.d/10-opcache.ini, not the CLI config. A production baseline sets opcache.enable=1, opcache.enable_cli=0, opcache.memory_consumption=256, opcache.interned_strings_buffer=32, opcache.max_accelerated_files=20000, opcache.validate_timestamps=0, opcache.revalidate_freq=0, opcache.jit=1255, opcache.jit_buffer_size=128M, and opcache.max_wasted_percentage=10. Adjust memory upward for large Laravel 12 or Laravel 13 codebases with extensive vendor trees. Confirm active settings with php -i | grep opcache.enable on FPM before tuning further.

A small codebase may survive on 128 MB, but Laravel 12 or Laravel 13 apps with large vendor directories typically need 256 to 512 MB of opcache.memory_consumption. Check real usage with opcache_get_status() and inspect used versus free memory plus num_cached_scripts. If used memory stays above 90%, raise the allocation. Watch opcache_statistics.cache_full and oom_restarts in status output, which signal silent evictions that gradually degrade performance as packages accumulate. Undersized OpCache wastes effort on database and Redis tuning because workers keep recompiling evicted scripts.

WordPress 7.1 with WooCommerce 11.1 and a moderate plugin set typically needs 128 to 192 MB. Heavy plugin stacks or page builders may need 256 MB.

Set opcache.validate_timestamps=0 in production. When enabled, PHP stat-checks files on every request, adding filesystem overhead that defeats much of OpCache’s benefit on stable traffic. The trade-off is that PHP will not auto-detect file changes. You must reload PHP-FPM after every deploy. That pairing is correct for production: disable timestamp validation, deploy code, then invalidate cache via FPM reload. Development should keep validate_timestamps=1 for automatic recompilation. Never rely on toggling this setting back on in production as a deploy strategy because it reintroduces stat overhead across all requests.

With opcache.validate_timestamps=0, production OpCache invalidation requires a PHP-FPM reload after every code deployment. Add it to your deploy pipeline. With Deployer 7, a typical task runs sudo systemctl reload php8.5-fpm after the deploy:symlink step. Use reload, not restart, for zero-downtime on a correctly configured pool. Reload gracefully replaces workers while a full restart drops active connections. Alternatives like opcache_reset() or opcache_invalidate() exist but are impractical or risky for full deploys. An authenticated Artisan command calling opcache_reset() is useful as an emergency tool, not a primary deploy step.

The default of 10,000 is too low for most modern frameworks. Laravel projects with many Composer packages can exceed 10,000 PHP files easily. Count files on the server with find on your application path, then set opcache.max_accelerated_files to at least 1.5 times that total. A production baseline of 20,000 or higher is common on Laravel 12 and Laravel 13 deployments. If the limit is too low, scripts get evicted silently and performance degrades as the codebase grows. Verify num_cached_scripts in opcache_get_status() matches your expected file count after deploy.

Preloading is optional but powerful for large PHP 8.x apps. At FPM startup, a preload script loads specified classes into OpCache permanently so every worker inherits them without lazy compilation on first request. Laravel 12 and Laravel 13 ship a stub at preload.php in the project root. Enable it with opcache.preload pointing at the stable current symlink path and opcache.preload_user=www-data. A syntax error in the preload script prevents FPM from starting, so test on staging that mirrors production OpCache settings first. Never point preload at an absolute path inside a release directory because it breaks on the next symlink deploy.

The recommended production setting is opcache.jit=1255 with opcache.jit_buffer_size between 64 and 128 MB. JIT translates hot opcode paths into native CPU instructions, which helps CPU-bound code but does little for typical CRUD apps waiting on MySQL or Redis. Most eCommerce and CMS workloads, including WooCommerce 11.1, Magento 2.4.x, and custom Laravel carts, are I/O-bound. OpCache itself delivers the big win; JIT is icing. Measure with load testing, comparing latency with JIT on and off. If p95 response time does not move, your bottleneck is elsewhere. JIT buffer shares the OpCache memory segment across FPM workers in a pool.

CLI and FPM use separate PHP configuration on Ubuntu. Running php artisan may show OpCache active while web requests do not use it because you edited the wrong ini file. Fix /etc/php/8.5/fpm/conf.d/10-opcache.ini for web traffic, not the CLI equivalent. Confirm with php -i | grep opcache.enable on the FPM SAPI and check phpinfo output from a web request. I have seen production servers where opcache.enable=1 on CLI gave a false sense of security while FPM left compile overhead on every page load. Always tune and verify the FPM config for real traffic.

Stale code after deploy is the most frequent incident: new files are on disk but behaviour is unchanged because validate_timestamps=0 and no FPM reload ran. OpCache disabled on FPM but enabled on CLI is another repeat pattern. Undersized opcache.memory_consumption causes silent evictions as vendor packages grow. A wrong opcache.preload path inside a release directory breaks after the next Deployer symlink swap. Servers running PHP 8.3 and 8.5 side-by-side need separate OpCache configs per version, each with its own shared memory segment. I have encountered these on sister sites sharing a Deployer 7 pipeline where the symlink swapped correctly but PHP-FPM never reloaded.

Run opcache_get_status() after deploy and during monthly performance audits. Confirm opcache.enable=1 in FPM phpinfo output. Check that used memory stays below roughly 85 percent of allocated opcache.memory_consumption. Verify num_cached_scripts matches your expected PHP file count. Confirm the FPM reload step ran in deploy logs. Watch for oom_restarts incrementing, which signals undersized memory. Also monitor cache_full in opcache_statistics. Add these checks to your server maintenance checklist alongside database indexing and application caching reviews. On projects I maintain, OpCache monitoring is step one before deeper query or Redis work.

OpCache itself is not a security vulnerability. The operational risk is stale cached bytecode after deploy: patched files stay inactive until FPM reload when validate_timestamps=0. That can leave fixed logic unexecuted until someone notices. Never expose opcache_reset() via a publicly accessible web endpoint. Treat FPM reload as a required deploy step in CI/CD, not optional. An authenticated Artisan command for emergency cache clearing is acceptable. The security concern is process discipline around invalidation, not OpCache mechanics. Ship fixes and reload PHP-FPM in the same deployment window every time.

Yes. Mount your OpCache ini as a config volume or bake it into the image. Reload the container PHP-FPM process after code updates. In multi-stage Docker builds for Laravel, OpCache config belongs in the runtime stage alongside FPM pool settings.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: