skip to content

In PHP, what does the realpath cache store, and how do realpath_cache_size, realpath_cache_ttl and open_basedir affect it?

level: middleimportance: should knowfreq 26%

answer

  1. resolved paths, not file contents
  2. per worker process, kept across requests
  3. 4M and 120 seconds by default
  4. open_basedir switches it off
  5. symlinked release directories

basics

~20 s

The realpath cache stores resolved absolute paths for files and directories PHP touches, saving repeated filesystem lookups. It is sized by realpath_cache_size (default 4M), expires entries after realpath_cache_ttl (default 120 s), and is disabled when open_basedir is set.

solid answer

~40 s

Every `include`, `require`, autoload and file function must turn a relative or symlinked path into a real absolute path, which means `stat` and `readlink` calls for each path component. The **realpath cache** remembers those results. It is per process and survives between requests, so a PHP-FPM worker resolves a path once and reuses it. `realpath_cache_size` (default `4M`, the total bytes of stored paths plus entry data) caps it, and `realpath_cache_ttl` (default `120` seconds) sets how long an entry is trusted. Both are `INI_SYSTEM`. Two traps: setting **`open_basedir` disables the cache**, so every lookup hits the filesystem; and with a symlink-switch deploy (`current -> releases/42`), workers can keep resolving `current` to the old release until their entries expire. `realpath_cache_get()` and `realpath_cache_size()` show what a worker holds.

code

php · 13 lines
php
<?php
declare(strict_types=1);

// Run inside a PHP-FPM request: the CLI has its own cache.
header('Content-Type: text/plain');

printf("realpath cache used: %d bytes of %s\n",
    realpath_cache_size(),
    ini_get('realpath_cache_size'));
printf("entries: %d, ttl: %s s, open_basedir: %s\n",
    count(realpath_cache_get()),
    ini_get('realpath_cache_ttl'),
    ini_get('open_basedir') ?: '(none)');

go deeper

for a junior

Know that PHP caches resolved file paths so repeated includes are cheaper, and that it is separate from OPcache.

for a middle

Explain per-process scope and persistence across requests, the defaults of realpath_cache_size and realpath_cache_ttl, and that open_basedir disables the cache.

for a senior

Diagnose mixed old and new code after a symlink deploy and fix it with resolved paths from the web server or an FPM reload, rather than by shortening the TTL.

for a principal

Choose a deploy layout and isolation model with these caches in mind, for example weighing open_basedir's safety check against its per-request filesystem cost.

## What gets resolved Before PHP can open `src/Controller/ProductController.php`, it needs the canonical absolute path: resolve the current directory, follow every symbolic link, collapse `.` and `..`. That takes a system call for each component of the path. A modern application touches hundreds of files per request through Composer's autoloader, templates and configuration, so those lookups add up. The **realpath cache** stores the result of each resolution: the path as given, the resolved real path, and a little metadata such as whether it is a directory. It does **not** store file contents or compiled code; that is OPcache's job. ## Scope and lifetime - The cache lives in the **globals of each PHP process** (each PHP-FPM worker has its own). - It is **not cleared at the end of a request**. A worker keeps its entries across the requests it serves, until they expire. - Entries expire after `realpath_cache_ttl` seconds. - `clearstatcache(true)` clears the realpath cache of the current process only, not other workers'. ## The directives | Directive | Default | Changeable | Meaning | |---|---|---|---| | `realpath_cache_size` | `4M` | `INI_SYSTEM` | bytes for stored path strings plus entry data | | `realpath_cache_ttl` | `120` | `INI_SYSTEM` | seconds an entry is trusted | The size is **bytes, not a number of entries**: long paths take more room. The default rose from `16K` to `4M` in PHP 7.0.16 and 7.1.2, so advice to raise it dates mostly from older versions. To see whether it is big enough, read `realpath_cache_size()` (bytes currently used) and `realpath_cache_get()` (the entries) from a web request, because a CLI process has its own cache. ## Trap 1: open_basedir disables it When `open_basedir` is set, PHP sets the realpath cache size to `0`. The shipped php.ini files state it: "if open_basedir is set, the cache is disabled". The effect is that every file access resolves its path from scratch, on every request. On shared hosts that rely on `open_basedir` for isolation, that is part of the price; on a dedicated server, container-level isolation usually serves better. ## Trap 2: symlink-switch deploys A common deploy layout: ``` /var/www/releases/41/ /var/www/releases/42/ /var/www/current -> /var/www/releases/42 ``` The deploy builds release 42 and then switches `current`. The web server's document root, or the script path it passes to PHP-FPM, points at `/var/www/current/public/index.php`. After the switch: 1. A worker that resolved `/var/www/current/...` a minute ago has the entry `/var/www/releases/41/...` cached. 2. For up to `realpath_cache_ttl` seconds, that worker keeps loading files from release 41. 3. Different workers flip at different times, so for a while requests are served by a **mix of old and new code**. The usual remedies: - have the web server pass the **resolved** path (for example, nginx's `$realpath_root` instead of `$document_root` in `SCRIPT_FILENAME`), so each request names the concrete release directory; - or reload PHP-FPM after the switch, which starts fresh workers with empty caches; - keep in mind that OPcache has its own view of file paths, which its own settings govern. ## Why autoloading makes it matter A Composer PSR-4 autoloader maps a class name to a candidate file path and checks whether that file exists; applications with many namespaces may check several candidates per class. Every check is a path resolution. With a warm realpath cache in a long-lived PHP-FPM worker, those checks are cheap lookups in memory. With the cache disabled by `open_basedir`, or too small for the application's paths, each request repeats them against the filesystem. That is one reason an application can be measurably slower on a host with `open_basedir` than on one without, with no code change at all. ## When to tune it - Raise `realpath_cache_size` only if `realpath_cache_size()` sits at the limit under real traffic; on PHP 8 the default fits most applications. - Raise `realpath_cache_ttl` on servers whose files change only by deploy, where every deploy reloads PHP-FPM anyway. - Leave `open_basedir` off unless you rely on it, and know that setting it trades filesystem performance for that check.

  • After switching the current symlink to a new release, why does clearstatcache(true) in one request not fix the mixed-version problem?
    `clearstatcache(true)` clears the realpath cache of the process that calls it, and each PHP-FPM worker has its own cache. One worker is fixed; the rest keep their old entries until `realpath_cache_ttl` expires. Passing the resolved path from the web server, or reloading PHP-FPM after the switch, fixes all workers.

saying these in an interview costs you the question

  • Thinks the realpath cache stores file contents or compiled scripts.
  • Believes the realpath cache is shared by all PHP-FPM workers like APCu.
  • Reads realpath_cache_size as a maximum number of cached paths.
  • Assumes the realpath cache still works when open_basedir is set.
  • Switches a current symlink and expects every worker to serve the new release immediately.