How to configure OPcache in PHP production for real performance gains

Technical guide to configure PHP OPcache in production for real performance: parameters, memory sizing, validation and troubleshooting.

OPcache is the bytecode cache extension bundled with PHP since version 5.5. Without it, PHP reads, parses and compiles each .php file on every request — work that repeats millions of times a day in production. With OPcache enabled and well configured, the compiled bytecode lives in shared memory and each request reuses the result, cutting latency and CPU usage significantly.

This tutorial is for PHP developers running applications in production (Laravel, Symfony, WordPress, or custom code) who have not yet enabled OPcache, or enabled it with default configuration and don’t know if it is properly sized. Estimated execution time: 20-30 minutes, including the measurement and tuning part.

The focus is practical: parameters that matter, reasonable starting values, how to verify that the cache is actually being used and how to invalidate bytecode after deploy without bringing PHP-FPM down. Everything tested on PHP 8.3 over Ubuntu 24.04 LTS, but the principles apply to any 7.4+ version on any distribution.

Prerequisites

What you need

Linux server with PHP 7.4 or higher, sudo access, PHP-FPM running (or mod_php on Apache) and your application already in production. OPcache comes bundled with PHP since 5.5 — you don’t need to install a separate package on most distros. Have access to php.ini or the conf.d directory.

Minimum PHP 7.4
Recommended PHP 8.3
Extra RAM 256-512 MB
Config file conf.d/10-opcache.ini

Confirm that OPcache is available before configuring. If the command below does not list opcache, you need to install the php-opcache package (Debian/Ubuntu) or php-opcache (RHEL/Alma/Rocky).

php -m | grep -i opcache

If the output is Zend OPcache, it is available and you only need to enable and tune it. If it is empty, install it first.

Check the current state

Before changing any parameter, record how OPcache is right now. That becomes your baseline to compare later.

01

List the current OPcache configuration:

php --ri opcache

The output shows each directive and the effective value. Look for opcache.enable, opcache.memory_consumption, opcache.max_accelerated_files and opcache.validate_timestamps. If enable is Off, the cache is not running — that explains much of the slowness.

02

See the status at runtime via PHP-FPM (not CLI):

Create a temporary file /var/www/opcache-status.php with:

<?php
header('Content-Type: application/json');
echo json_encode(opcache_get_status(false), JSON_PRETTY_PRINT);

Access via internal curl:

curl http://127.0.0.1/opcache-status.php | head -30

Note the values of memory_usage.used_memory, memory_usage.free_memory, opcache_statistics.hits and opcache_statistics.misses. If hits is zero or very low compared to misses, the cache is misconfigured or freshly started.

Delete the file afterwards — don’t leave it exposed in production.

Create or edit the OPcache-specific configuration file. On Ubuntu/Debian it lives in /etc/php/8.3/fpm/conf.d/10-opcache.ini (adjust the version).

03

Edit the configuration file:

sudo nano /etc/php/8.3/fpm/conf.d/10-opcache.ini

Replace the content with a solid production configuration:

zend_extension=opcache.so

opcache.enable=1
opcache.enable_cli=0

opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000

opcache.validate_timestamps=1
opcache.revalidate_freq=60

opcache.save_comments=1
opcache.fast_shutdown=1
opcache.huge_code_pages=0

opcache.max_wasted_percentage=10
opcache.consistency_checks=0

Each parameter has a specific reason — explanation below the reference table.

Reference for the main parameters:

DirectiveSuggested valueWhat it does
opcache.memory_consumption256Shared memory (MB) for bytecode
opcache.interned_strings_buffer16Buffer (MB) for deduplicated strings
opcache.max_accelerated_files20000How many files can be cached
opcache.validate_timestamps1If 0, never revalidates — requires manual reset
opcache.revalidate_freq60Interval (s) between mtime checks
opcache.save_comments1Keeps DocBlocks — required for Laravel/Symfony
opcache.fast_shutdown1Faster deallocation at request end
save_comments must stay at 1

Modern frameworks (Laravel, Symfony, Doctrine) use annotations and PHP 8 attributes that read DocBlocks at runtime. Setting opcache.save_comments=0 breaks DI containers, validation and ORM mapping. The memory gain is not worth the risk.

04

Apply the configuration by restarting PHP-FPM:

sudo systemctl restart php8.3-fpm

After the restart, wait a few seconds for real requests to warm up the cache. On hosts with Apache + mod_php, use sudo systemctl restart apache2.

Sizing memory_consumption correctly

256 MB is a reasonable starting point, but the ideal memory depends on the size of your codebase. A typical Laravel application with the full vendor/ directory consumes 80-150 MB. WordPress with 30 plugins can exceed 200 MB.

05

Measure the real consumption after the cache warms up (let the app run for 10-15 minutes with normal traffic):

php -r 'print_r(opcache_get_status(false)["memory_usage"]);'

The output looks like:

Array
(
    [used_memory] => 142336512
    [free_memory] => 125829120
    [wasted_memory] => 0
    [current_wasted_percentage] => 0
)

If free_memory stays below 20% of the total allocated, increase opcache.memory_consumption to 384 or 512 MB. If wasted_memory climbs above 10%, consider adjusting opcache.max_wasted_percentage or scheduling a reset during low-traffic windows.

interned_strings_buffer matters more than it looks

Object-oriented frameworks create many repeated class, method and property names. The interned strings buffer deduplicates that. In large Laravel apps, going from 8 MB to 16 or 32 MB reduces fragmentation and frees memory from the main bytecode pool.

Deploy strategy: invalidating the cache

With validate_timestamps=1 and revalidate_freq=60, changes in PHP files are only detected after 60 seconds. For atomic deploys, that is not enough — you need to invalidate the cache at deploy time.

06

Install the cachetool tool to invalidate the cache without restarting PHP-FPM:

curl -sO https://gordalina.github.io/cachetool/downloads/cachetool.phar
chmod +x cachetool.phar
sudo mv cachetool.phar /usr/local/bin/cachetool

Add it to your deploy script, after git pull or rsync:

cachetool opcache:reset --fcgi=/var/run/php/php8.3-fpm.sock

The reset is instant and does not drop active connections — unlike systemctl reload.

Don't use validate_timestamps=0 without an invalidation pipeline

Mode validate_timestamps=0 is more performant (zero stat() per request), but if you forget to invalidate the cache after deploy, the server serves old code indefinitely. Only use 0 if your deploy script has guaranteed opcache_reset() or cachetool opcache:reset.

Verifying the real gain

Configuring without measuring is faith, not engineering. Compare latency and CPU before and after.

07

Run a simple benchmark with ab (Apache Bench) or wrk against a representative endpoint:

ab -n 1000 -c 10 https://yourdomain.com/api/products

Compare the Time per request before and after enabling OPcache. Reductions of 30-60% in Laravel/Symfony apps are common. In apps that depend heavily on I/O (database, external APIs), the percentage gain is smaller because PHP is not the bottleneck.

08

Confirm that hits are dominating misses:

php -r 'print_r(opcache_get_status(false)["opcache_statistics"]);'

The hits / (hits + misses) ratio should pass 99% after the cache warms up. If it stays below 95%, probably max_accelerated_files is too low or the cache is being reset frequently.

Troubleshooting

Cache fills up quickly and wasted_memory spikes

A sign that max_accelerated_files is too low or that max_wasted_percentage has been hit. OPcache starts discarding entries and refragments the memory. Increase max_accelerated_files to 30000-50000 and memory_consumption by 50%.

opcache_get_status() returns false in CLI scripts

Safe default: opcache.enable_cli=0 disables OPcache on the CLI. If you need to inspect via CLI, temporarily set it to 1. In production, keep 0 — CLI scripts are short and the cache does not help.

Code changes don’t appear after deploy

Either validate_timestamps=0 without a programmatic reset, or another cache (Redis/file cache from the application) is serving old data. Run cachetool opcache:reset and clear the application cache (php artisan cache:clear on Laravel, for example).

Next steps

Once OPcache is stable, the next gain usually comes from tuning PHP-FPM (pool size, pm.max_children) to keep up with the larger throughput the server now handles. It is also worth evaluating OPcache JIT in apps with a CPU-intensive component and reviewing HTTP cache headers in nginx to reduce pressure on PHP even further.

If you are running on shared hardware and the performance gain hits the plan resource limits, a Hostini VPS with dedicated KVM gives full control over PHP-FPM, OPcache memory and kernel parameters — without CPU contention with noisy neighbors.

Frequently asked questions

Is OPcache worth it for small applications with low traffic?

Yes. Even at low RPS, OPcache eliminates the repeated work of parsing and compiling on every request. The percentage latency gain is the same or larger in small apps — only the absolute impact is smaller. Enabling it is practically free: a few MB of RAM in exchange for less CPU.

What is the difference between opcache.validate_timestamps=0 and validate_freq=60?

With validate_timestamps=0 OPcache never checks whether the PHP file changed — you need to reset it manually after deploy. With validate_timestamps=1 and revalidate_freq=60 it checks every 60s. Mode 0 is faster but requires a deploy pipeline that invalidates the cache.

How do I invalidate OPcache after a deploy without restarting PHP-FPM?

Use opcache_reset() via a PHP script accessed by CLI or authenticated HTTP, or cachetool (cachetool opcache:reset --fcgi=/var/run/php/php8.3-fpm.sock). Restarting PHP-FPM works but drops in-flight connections — programmatic reset is cleaner in production.

How much memory should I allocate to opcache.memory_consumption?

Start with 256 MB for medium applications (Laravel, Symfony, WordPress with plugins). Monitor via opcache_get_status(): if memory.free_memory drops below 10% or wasted_memory climbs too much, increase it. Modern frameworks like Laravel easily consume 128-200 MB of cache.

Is OPcache JIT (PHP 8+) worth it for web applications?

For traditional web apps (Laravel, WordPress) the JIT gain is modest, 5-15% in benchmarks. JIT shines on CPU-bound workloads: image processing, mathematical calculations, complex parsing. Enable it with opcache.jit=tracing and opcache.jit_buffer_size=128M if your app has a heavy computational component.

Why does opcache_get_status() return false even with OPcache enabled?

Usually it is opcache.restrict_api configured for a specific path that does not include the calling script, or opcache.enable_cli=0 when running via CLI. Check opcache.restrict_api in php --ri opcache and use opcache.enable_cli=1 for command-line tests.

Topics:
Next steps Ryzen cloud with NVMe storage and always-on DDoS protection.Go live on a Hostini VPS →
Was this tutorial helpful?
Chat on WhatsApp