In PHP, why can is_file() or filesize() keep returning an old result inside one script, and when is clearstatcache() actually needed?
answer
- stat() results are cached per script
- one entry: the last path stat'ed
- PHP's own writes clear it
- failed lookups are not cached
- clearstatcache(true, $path) also drops realpath entries
basics
~20 sPHP caches the last stat() result, and is_file(), filesize(), filemtime() and similar functions reuse it for the same path. If another process changes that file, repeated checks in one script can see old data until clearstatcache() is called.
solid answer
~40 sFunctions such as `stat()`, `is_file()`, `is_dir()`, `file_exists()`, `filesize()`, `filemtime()` and `is_writable()` go through a **stat cache**. In PHP 8.5's source it holds one entry for the last path passed to `stat()` and one for the last `lstat()`, and a repeated check on that same path is answered from memory. PHP clears it itself after most of its own file operations: opening or writing a plain file, `unlink()`, `rename()`, `rmdir()`, `chmod()`, `touch()`. Failed lookups are not cached, so waiting for a file to *appear* works. The stale case is a script that re-checks one path while **another process** changes it: polling `filesize()` until an external renderer finishes, or a long-running worker watching a config file's `filemtime()`. There you call `clearstatcache()` before each check; `clearstatcache(true, $path)` also drops that path's realpath cache entry.
go deeper
Recall that is_file(), filesize() and filemtime() share a stat cache and that clearstatcache() empties it.
Explain what is cached and when PHP clears it itself, and why only changes made by other processes produce stale results.
Show you spot the polling and long-running-worker cases, place clearstatcache() right before the fresh check, and prefer rename-into-place over polling.
Treat file-based signalling between processes as a design smell: prefer atomic renames, locks or a queue, and document which caches each worker must refresh.
## What the stat cache is Every call like `is_file($p)` or `filesize($p)` needs the operating system's `stat()` data for the path. To avoid asking twice in a row, PHP keeps the last answer in a per-script **stat cache**. The manual lists the functions that read from it: - `stat()`, `lstat()` - `file_exists()`, `is_file()`, `is_dir()`, `is_link()` - `is_readable()`, `is_writable()`, `is_executable()` - `filesize()`, `filemtime()`, `filectime()`, `fileatime()`, `fileinode()`, `filegroup()`, `fileowner()`, `filetype()`, `fileperms()` ## How big it is and when PHP clears it The php-src 8.5 implementation is smaller than the manual's wording suggests: | Property | Behaviour in php-src 8.5 | |---|---| | Entries | one for the last `stat()` path, one for the last `lstat()` path | | Hit | the next check on the **same path string** reuses the stored result | | Miss | any other path replaces the entry | | Failed lookups | not stored, so a missing file is re-checked every time | | Cleared by PHP itself | opening or writing a plain file, `unlink()`, `rename()`, `rmdir()`, `chmod()`, `chown()`, `chgrp()`, `touch()` | | Cleared by you | `clearstatcache()` | So within your own script, changing a file through PHP and then checking it again is safe. The cache only lies when **something outside the script** changes the path it looked at last. ## Where stale results actually bite 1. **Waiting for another process to finish a file.** A worker calls an external PDF renderer, then polls `filesize($pdf)` until it stops growing. Every poll hits the cache and returns the first size. 2. **Waiting for a file to disappear.** `while (file_exists($lock)) { usleep(100_000); }` keeps seeing the file after another process deleted it, because the successful lookup was cached. (Waiting for a file to *appear* works, since failures are not cached.) 3. **Long-running CLI workers.** A daemon that reloads its configuration when `filemtime($config)` changes never sees the change if the same path is its last stat. 4. **Checking after a shell command.** `exec('convert …')` modifies a file; the next `filesize()` on that path can still report the old value. The fix is to call `clearstatcache()` immediately before the check that must be fresh: ```php <?php declare(strict_types=1); $pdf = '/var/app/invoices/INV-2026-0912.pdf'; $last = -1; while (true) { clearstatcache(); $size = is_file($pdf) ? filesize($pdf) : 0; if ($size > 0 && $size === $last) { break; // size stable: the renderer is done } $last = $size; usleep(200_000); } ``` Polling for a stable size is itself a heuristic; the robust design is for the producer to write under a temporary name and `rename()` into place, so the consumer only ever sees complete files. ## clearstatcache() arguments ```php clearstatcache(bool $clear_realpath_cache = false, string $filename = ""): void ``` - With no arguments it clears the stat cache. - With `true` it also clears the **realpath cache**, which maps paths to their resolved form (following symlinks). That cache lives in the PHP process and, under PHP-FPM, survives between requests. - With `true` and a filename it drops only that path's realpath entries; the filename is ignored unless the first argument is `true`. The realpath cache matters for symlink-switch deploys: after a `current` symlink is repointed, a long-lived process can keep resolving the old target until its entries expire or are cleared. Sizing and TTL of that cache are a performance topic of their own. ## Diagnosing a suspected stale check 1. Confirm another process changes the path; if only your script touches it, the cache is not the cause. 2. Check whether the stale path is the **last** one stat'ed before the repeat check; a check on any other path in between replaces the entry. 3. Add `clearstatcache()` right before the check and compare the results. 4. If the fix works, ask whether polling is the right design at all, or whether the producer should signal completion by renaming into place. ## What clearstatcache() does not fix - It does not make a check-then-act sequence safe. `if (!file_exists($p)) { file_put_contents($p, …); }` is still a race between two processes; use `fopen($p, 'x')` or rename-into-place instead. - It does not refresh data inside an open handle; `fstat($h)` asks the operating system directly for that handle. - Calling it before every check in a hot loop costs little, but it is only needed when another process may have changed the path in between.
- Why does waiting for a file to appear work without clearstatcache(), while waiting for it to disappear does not?PHP only stores successful stat results. A missing file is looked up again on every call, so `file_exists()` flips to `true` as soon as the file exists. Once it has been found, the positive result is cached, so a loop waiting for deletion by another process keeps seeing `true` until `clearstatcache()` runs or another path replaces the entry.
- Does unlink() or file_put_contents() in the same script leave a stale stat cache behind?No. PHP clears the stat cache itself when it opens or writes a plain file, unlinks, renames, removes a directory, or changes permissions, ownership or times. The manual mentions `unlink()` explicitly. Staleness comes from changes made outside the script, such as another worker or an external command.
- What does clearstatcache(true, $path) add over clearstatcache()?The first argument also clears the realpath cache, which remembers how paths resolve through symlinks; with a filename it clears only that path's entries. That matters when a symlink has been repointed, for example after a deploy switches a `current` link, and a long-lived process must resolve the new target.
The stat cache is a sticky note with the answer to the last question you asked about one file. Your own edits tear the note off, but if a colleague changes the file, you keep reading the old note until you throw it away with clearstatcache().
saying these in an interview costs you the question
- PHP caches stat results across requests, so every request sees stale data.
- You must call clearstatcache() after every unlink() in your own script.
- file_exists() will keep returning false after another process creates the file.
- clearstatcache() makes a file_exists() check followed by a write race-free.
- The stat cache stores results for every path the script has checked.