The Ghost in the Opcode: A Practical Guide to Tuning APC, APCu, and PHP Caches Without Losing Your Sanity
Every site has a subconscious. Ours is a few hundred megabytes of compiled PHP sitting in shared memory, humming away, deciding whether your visitors get a page in 40 milliseconds or stare at a spinner long enough to reconsider their life choices.
We call it a cache. Really it's more like the site's dreaming mind: everything your code has already figured out, held in RAM so it doesn't have to figure it out again.
And if you've been chasing down an old tutorial about "APC" or "alternate PHP cache" — congratulations, you've stumbled into one of the most confused corners of PHP performance. Let's untangle it.
First, the awkward truth about APC
APC, the Alternative PHP Cache, was the go-to opcode cache for PHP for years. It cached compiled bytecode so the interpreter didn't have to re-parse your files on every request. It also had a user cache bolted on the side, which people used for storing arbitrary data.
APC is gone. It hasn't been supported by modern PHP for a long time now, and if you're running anything resembling a current stack in 2026, you're not running APC. You're running its two descendants:
- OPcache, which ships bundled with PHP and handles the opcode caching job.
- APCu, which is the user cache half of the old APC, kept alive as a standalone extension.
So when someone searches for "tuning APC," what they usually need is one of two things: OPcache tuning (the performance layer), or APCu tuning (the data layer). Sometimes both. Treat them separately — they have different knobs and different failure modes.
OPcache: where the real speed lives
OPcache is the thing that makes PHP not feel like PHP. Out of the box, it's often enabled with conservative defaults that leave performance on the table. Here's the tuning that actually matters.
opcache.memory_consumption
How much shared memory OPcache gets. Default is typically 128MB, which is fine for small sites and cramped for anything with a real codebase — think a large WordPress install with plugins, or a framework-heavy app.
Start at 128MB. Check opcache_get_status() and look at used_memory plus wasted_memory. If wasted_memory creeps past 10–15% or you're near the ceiling, bump to 256MB. Going to 512MB "just in case" is mostly waste on a normal site, though on a box running many vhosts it can make sense.
opcache.max_accelerated_files
The cap on how many PHP files can be cached. The default (a few thousand) is a trap for modern apps — a medium WordPress site with a page builder can blow past 10,000 files without breaking a sweat.
A simple trick: count your PHP files.
find /path/to/site -name '*.php' | wc -l
Then round up to the next available value. OPcache only accepts numbers from a fixed set (roughly 200, 400, 1000, 2000, 4000, 8000, 16000, 32000...). Most real sites want 16000 or 32000. This single change fixes a shocking number of "why does my site feel slow" mysteries.
opcache.interned_strings_buffer
Shared memory for interned strings. Default 8MB. If you run a big app, 16–32MB is a normal, cheap upgrade. It's not glamorous, but it quietly reduces memory churn.
opcache.validate_timestamps and revalidate_freq
This is the tradeoff everyone gets wrong. With validate_timestamps=1, PHP checks whether files changed. With it off (0), PHP never checks — fast, but you must reload PHP-FPM after every deploy or you'll serve stale code to real humans.
For production with a proper deploy pipeline: set validate_timestamps=0 and reload PHP-FPM as part of your deploy. For a site someone edits live or a staging box: leave it on and set revalidate_freq to something sane like 2–60 seconds.
opcache.jit
PHP 8's JIT compiler is the shiny object everyone asks about. The honest answer in 2026: for typical web request/response workloads, JIT gives modest gains and occasionally costs you memory. It shines on CPU-heavy computation. Try opcache.jit=tracing with a modest buffer if you're curious, measure with real traffic, and don't be precious about turning it back off.
APCu: the other half of the ghost
APCu is the user cache — the part where you store your own data, not compiled code. It's fast, it's shared memory, and it's per-server, which means it does not exist in a multi-server setup unless you like inconsistency.
Key knobs:
- apc.shm_size — how much memory APCu gets. 32–128MB covers most use cases.
- apc.ttl — time-to-live for entries in seconds. Set it to match how stale your data can tolerate being.
- apc.num_files_hint — legacy, mostly ignorable now.
- apc.enable_cli — leave off in production, turn on temporarily when debugging from the command line.
One rule: APCu is a cache, never a source of truth. If losing it on restart breaks your app, you've built something fragile. Wrap every apcu_fetch() in a fallback path.
The diagnostic habits that beat any config file
Configs are cheap. Knowing what your cache is actually doing is the real skill.
- Dump OPcache status. A tiny PHP script calling
opcache_get_status()tells you hit rate, memory used, wasted memory, and cached file count. Check it after a deploy and a week later. - Watch the hit rate. If it's below ~99% in steady state, something's wrong — usually too few accelerated files or a memory ceiling being hit.
- Reload after deploy.
systemctl reload php-fpm. Put it in your pipeline. Not optional if timestamps are off. - Don't tune blind. Change one value, measure, keep or revert. Cargo-cult configs from 2019 blog posts are how servers end up with 1GB of OPcache and 40MB of actual code.
The bigger point
Caching is the least glamorous form of craft and the most reliable one. Nobody will ever compliment your opcache.max_accelerated_files value. But the difference between a site that feels alive and one that feels like it's thinking about it is usually just a few hundred megabytes of shared memory doing the remembering for you.
Tune it deliberately. Measure it. And remember the golden rule of the alternate PHP cache: the fast path is invisible, which is exactly how you know it's working.