Web Development

PHP OPcache During Deployments - Make Fresh Code Deliberate, Not Accidental

PHP OPcache During Deployments - Make Fresh Code Deliberate, Not Accidental

A deployment replaces PHP files on disk, but that does not by itself answer which compiled instructions a long-running PHP process will execute on the next request. If OPcache is involved, when does new code become active: immediately, after a short interval, after an explicit invalidation, or only after the relevant process restarts?

The useful answer is not “always clear the cache.” It is to choose a freshness contract that matches the deployment method, then verify that contract in the same runtime that serves web requests. This article examines that narrow operational problem for a small PHP application. It does not promise zero-downtime deployment, prescribe one PHP-FPM service name, or estimate performance gains that have not been measured.

OPcache has one specific job

PHP normally parses and compiles a script before executing it. The official OPcache introduction explains that OPcache stores precompiled script bytecode in shared memory, avoiding repeated loading and parsing on later requests. That is its useful, limited role.

It is not an HTTP response cache, a database cache, Composer's package cache, or an application data cache. Purging a CDN does not refresh PHP bytecode. Running composer install does not, by itself, define when a long-running PHP runtime notices every changed script. Treating all of these layers as “the cache” makes deployment failures harder to locate.

OPcache also has more than one storage mode. Its normal in-memory cache and optional file cache are not interchangeable. This distinction matters later: the documented opcache_reset() function resets the in-memory cache, not the file cache.

Timestamp validation is a freshness policy

The central setting is opcache.validate_timestamps. According to the PHP OPcache runtime configuration, when this option is enabled, OPcache checks for updated scripts according to opcache.revalidate_freq. A frequency of 0 checks for updates on every request. A positive value permits an interval during which cached bytecode may continue to be used before another timestamp check.

; Illustrative policy: check script timestamps on every request.
opcache.validate_timestamps=1
opcache.revalidate_freq=0

This policy is easy to reason about for a modest site: publishing a complete new file allows OPcache to detect its changed timestamp on the next request. “Check every request” does not mean “disable OPcache”; the compiled bytecode can still be reused when the source has not changed. It may add work, but the cost depends on the application, filesystem, traffic, and PHP build. A benchmark from another server cannot decide whether that cost matters here.

A positive revalidation interval is another valid policy, provided the release process accepts its freshness window. If the interval is two seconds, for example, the contract is not “new code is instant.” It is that OPcache periodically checks according to the configured interval. Deployments and smoke tests should not claim stronger behavior than the configuration provides.

Turning validation off transfers responsibility

With opcache.validate_timestamps=0, PHP's manual says filesystem changes require a manual reset, targeted invalidation, or a relevant web-server restart before they take effect. This can remove timestamp checks from the request path, but it does not remove freshness work. It transfers that work to the deployment procedure.

; Use only when every release has a reliable refresh step.
opcache.validate_timestamps=0

That trade can be sensible in a controlled release system. It is less convincing when files are edited in place, releases are copied manually, or the operator cannot state which process owns the cache. In those conditions, disabled validation can preserve old bytecode indefinitely after the source changes. A setting copied as a “production optimization” has silently become a correctness dependency.

Configuration itself has a lifecycle. The PHP manual's configuration-file documentation notes that configuration loading depends on the Server API, or SAPI. Server-module PHP reads its configuration at startup, while CGI and CLI read it on invocation; SAPI-specific files and additional scanned INI files may also apply. Editing an INI file therefore does not prove that an already running web process has adopted it.

Invalidate, reset, and restart answer different questions

Invalidate one known script

opcache_invalidate() targets one script. Without its force argument, the function invalidates only when the source modification time is newer than the cached bytecode; with force=true, it invalidates regardless of that comparison. If OPcache's file cache is enabled, the documented function also invalidates the corresponding file-cache entry.

This is precise when a deployment knows the exact changed files. It becomes awkward when a release changes hundreds of scripts, removes files, regenerates an autoloader, or changes relationships among classes. A list of successful per-file calls is not automatically proof that the release is internally compatible.

Reset the in-memory cache

opcache_reset() resets the entire in-memory opcode cache. Scripts are loaded and parsed again as later requests reach them. The function does not reset the optional file cache, and it can return false when OPcache is disabled or a restart is pending or in progress.

A full reset is broader and simpler than enumerating files, but it also discards useful compiled entries and makes subsequent requests rebuild them. More importantly, a reset function must run in the cache context that matters. Creating an unauthenticated web endpoint that exposes reset capability is not a reasonable shortcut; it adds a control surface to production and can turn repeated requests into disruption.

Manage the serving process

A process lifecycle operation can provide a clearer boundary when timestamp validation is disabled or when configuration changes must be loaded. The exact command depends on the operating system, PHP packaging, SAPI, service manager, and availability requirements. Restarting a guessed service name from a generic tutorial is not a deployment design.

A restart may interrupt work; a graceful reload may allow old and new workers to overlap; and either can fail. The deployment therefore needs the actual unit definition, its documented behavior, a health check, fresh logs, and a rollback path. “The command exited zero” is narrower evidence than “the intended release now serves correct responses.”

Do not ask the CLI to speak for PHP-FPM

Commands such as these are useful, but they describe the CLI invocation:

php --ini
php --ri "Zend OPcache"

The runtime configuration documents opcache.enable_cli separately, and its documented default is disabled. The configuration-file rules also allow SAPIs to load different files. Consequently, CLI output may not describe the PHP-FPM workers serving the site. It is evidence about one runtime, not automatically all runtimes.

For the web runtime, collect the needed settings through a protected administrative diagnostic, deployment instrumentation, or configuration management. Do not publish a full phpinfo() page. It can reveal paths, modules, environment details, and configuration that need not be public.

Where access policy permits, opcache_get_status(false) can report state for the current in-memory cache instance without listing every cached script. Documented fields include whether the cache is enabled or full, restart state, memory use, and statistics. It does not report the file cache, and opcache.restrict_api can prevent access. These limits are part of interpreting the result, not inconveniences to bypass.

Fresh bytecode cannot repair unsafe file publication

OPcache's opcache.file_update_protection avoids caching files younger than a configured number of seconds. The PHP documentation presents it as protection against caching incompletely updated files and notes that it can be set to zero when all updates are atomic.

That setting should not be mistaken for a complete deployment transaction. If files are overwritten one by one in the live directory, different requests can observe a mixture of release states. One script may refer to a class or function that another file has not received yet. Timestamp checks can notice changes; they cannot make a multi-file copy atomic.

A safer publication mechanism builds a complete release away from the live path, validates it, and then performs a controlled switch. The exact design may use release directories, immutable images, or a maintenance window. Symlink-based releases deserve separate care because PHP also has a realpath cache. Path-resolution freshness and opcode freshness are related operationally but are not the same mechanism.

Three proportionate models for a small application

Model A: timestamp validation on, every request

Use validate_timestamps=1 and revalidate_freq=0, publish files safely, then send representative smoke requests. This favors a simple freshness rule over avoiding every timestamp check. It is a reasonable baseline when traffic is modest and no local measurement shows that validation is a meaningful bottleneck.

Model B: timestamp validation on, bounded interval

Use a positive revalidation frequency and explicitly accept the resulting window. Verification should wait or retry according to that policy rather than issuing one immediate request and concluding that the wrong release is permanent. The interval should be an operational decision, not an undocumented surprise.

Model C: timestamp validation off, release-controlled refresh

Disable validation only when every deployment has an authenticated, observable refresh step for the serving runtime. A release should fail closed if that step fails. This model can reduce request-path checks, but it couples correctness to the release system and increases the cost of bypassing it for a “quick edit.”

None of these models is universally best. The right choice depends on measured workload, deployment discipline, acceptable freshness delay, and how easily the serving process can be managed without harmful interruption.

A deployment checklist that produces evidence

  1. Record the serving SAPI, loaded INI files, timestamp policy, revalidation interval, and whether file cache is enabled.
  2. Define when new code is expected to become active under that policy.
  3. Build and validate a complete release before exposing it to requests.
  4. Publish through an atomic switch or an explicit maintenance procedure, rather than copying an unknown sequence of live files.
  5. If the policy requires invalidation, reset, or process management, run it through a protected deployment path and check its result.
  6. Observe cache or process state in the web-serving context, not only from CLI PHP.
  7. Request changed behavior, a nearby unchanged path, and a failure path; inspect fresh application and PHP-FPM logs.
  8. Keep the previous release and a tested restoration procedure.

A version marker exposed only through an authenticated health check can make this verification clearer. It should identify the release artifact, not disclose secrets or pretend that one marker proves every route works. The marker answers “which release responded”; behavioral checks answer whether that release works.

Conclusion

OPcache turns PHP source freshness into part of the deployment contract. With timestamp validation enabled, the contract includes the revalidation interval. With validation disabled, the release system must deliberately refresh the relevant runtime. Targeted invalidation, full reset, and process lifecycle operations are different tools, not interchangeable incantations.

For a small application, the most defensible policy is often the one that can be explained and verified with the fewest hidden assumptions. Optimize timestamp checks only after measurement justifies the added release dependency. Until then, knowing when new code becomes active is more valuable than making a cache-clearing command look sophisticated.

References