OPcache Internals: What Opcode Reuse Actually Saves
Understand at a conceptual level how OPcache stores compiled opcodes in shared memory keyed by file path, how timestamp validation decides whether a cached entry is still trustworthy, and exactly which pipeline stages opcode reuse skips -- as a short bridge into the deeper configuration and JIT material covered elsewhere in this course.
OPcache's whole value proposition rests on one idea: compiling PHP source into opcodes is real, repeatable work, and if the source hasn't changed there's no reason to redo that work on every request. This lesson stays at that conceptual level -- what gets cached, how it's keyed, and how staleness is detected -- rather than walking through every OPcache directive or the JIT compiler layered on top of it. For configuration tuning (memory sizing, validate_timestamps in production, preloading) and for the JIT compiler that builds on top of cached opcodes, see the 'OPcache and JIT' and 'OPcache for Performance' lessons in this course, both of which this lesson deliberately keeps short in favor of.
Shared memory, keyed by file path
When OPcache is enabled, the first request for a given .php file runs the full lex/parse/compile pipeline as normal, but instead of discarding the resulting opcodes when the request ends, OPcache stores them in a shared memory segment that every PHP-FPM worker process can read -- keyed by that file's absolute path. The next request for the same file, from any worker in the pool, finds the opcodes already sitting in shared memory and jumps straight to executing them, skipping the compile step (and the lex/parse steps before it) entirely.
Because the cache lives in shared memory rather than per-process memory, the benefit compounds across an entire FPM pool: one worker's first-request compile pays the cost once, and every other worker -- and every subsequent request to any worker -- reuses that same compiled result without ever touching the source file again, until the entry is evicted or invalidated.
• Cache key: the file's absolute path -- one cached opcode entry per distinct file. • Storage: a shared memory segment readable by every worker process in the pool, not per-process memory. • First request for a file: full lex/parse/compile, then the result is stored. Every later request: opcodes are reused directly.
Staying valid: opcache.validate_timestamps
A cached opcode entry is only useful if it still matches the source file it came from, and opcache.validate_timestamps controls how that's checked. With it enabled (the common default in development), OPcache compares the file's modification time against the cached entry on each request (throttled by opcache.revalidate_freq) and recompiles automatically if the file has changed -- convenient, but that timestamp check itself is a small amount of recurring work on every request. With it disabled (the recommended production setting), OPcache trusts the cache unconditionally and never looks at the file again once an entry exists, which removes even that small check -- but it also means a deployed code change will not take effect until the cache is explicitly cleared or the FPM workers are restarted.
This tradeoff -- convenience and always-fresh code in development, versus maximum performance with an explicit cache-clear step on every deploy in production -- is exactly the kind of configuration decision the 'OPcache and JIT' lesson walks through in full, alongside memory sizing and preloading; the short version here is just: know that this switch exists and what it trades away in each position.
• opcache.validate_timestamps = On: checks the file's mtime (throttled by revalidate_freq) and recompiles on change -- typical for development. • opcache.validate_timestamps = Off: never checks the file again once cached -- fastest, but a deploy needs an explicit cache clear or worker restart. Typical for production. • Full configuration guidance (memory sizing, revalidate_freq tuning, preloading, cache-clearing on deploy) lives in the 'OPcache and JIT' and 'OPcache for Performance' lessons.
Code Examples
validate_timestamps=0 is the setting that captures this lesson's core point in one line: OPcache stops even checking whether the source file changed, which removes the last per-request overhead of using the cache but shifts the responsibility for invalidating stale entries onto the deploy process itself. The full set of directives around this (memory_consumption sizing, max_accelerated_files, revalidate_freq) is covered with tuning guidance in the 'OPcache and JIT' lesson.
Common mistake: Setting validate_timestamps=0 in production without also wiring an opcache_reset() (or worker restart) into the deploy pipeline means code changes silently don't take effect until something eventually clears or evicts the stale cached opcodes.
; php.ini -- production-oriented OPcache settings (conceptual illustration)
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
; With validate_timestamps=0, a deploy must run:
; php -r "opcache_reset();"
; or restart PHP-FPM workers, or the old cached opcodes keep serving.Important Rules
OPcache's cache key is the file's absolute path; the value is that file's already-compiled opcodes, stored in shared memory shared by every worker in the pool.
Opcode reuse skips lexing, parsing, and compiling entirely for a cache hit -- only the execute stage still runs, against the previously compiled opcodes.
opcache.validate_timestamps=On checks mtimes and auto-recompiles on change (development); =Off trusts the cache unconditionally and needs an explicit clear/restart on deploy (production).
For directive-by-directive configuration, memory sizing, preloading, and the JIT compiler that runs on top of cached opcodes, see 'OPcache and JIT' and 'OPcache for Performance' rather than re-deriving it from this overview.
[multiple-choice] When OPcache serves a cache hit for a request, which stages of the normal compile pipeline actually still run? A. All four stages run again every time, just faster B. Only lexing and parsing run; compiling and executing are skipped C. Lexing, parsing, and compiling are all skipped -- only the execute stage runs, directly against the previously compiled opcodes retrieved from shared memory D. Only the execute stage runs, but against freshly re-read source text rather than opcodes
C. Lexing, parsing, and compiling are all skipped -- only the execute stage runs, directly against the previously compiled opcodes retrieved from shared memory — The entire point of caching opcodes is that the expensive lex/parse/compile work doesn't need to happen again once a valid cached entry exists for a file -- a cache hit goes straight to executing the already-compiled opcodes pulled from shared memory.
[multiple-choice] A team sets opcache.validate_timestamps=0 in production but forgets to add a cache-clearing step to their deploy pipeline. What happens after their next code deploy? A. OPcache automatically detects the new file content on the next request regardless of the setting B. The old cached opcodes keep being served, because with validate_timestamps off OPcache never checks the file again once an entry exists -- the deploy silently has no effect until the cache is explicitly reset or the workers are restarted C. PHP-FPM refuses to start until the cache is cleared D. OPcache disables itself automatically when it detects a deploy
B. The old cached opcodes keep being served, because with validate_timestamps off OPcache never checks the file again once an entry exists -- the deploy silently has no effect until the cache is explicitly reset or the workers are restarted — With validate_timestamps disabled, OPcache trusts a cached entry unconditionally and stops checking the source file's modification time altogether, which is exactly what makes it faster -- but it also means new code on disk is invisible to running workers until something explicitly resets the cache (opcache_reset()) or restarts the FPM pool.