skip to content

In PHP, how do you replace a file so that readers never see a half-written version, and when does rename() stop being atomic?

level: seniorimportance: should knowfreq 35%

answer

  1. write elsewhere, then swap
  2. tempnam() in the destination directory
  3. chmod() the 0600 temp file
  4. same file system or copy-and-unlink
  5. copy() is never atomic

basics

~20 s

Write the new content to a temporary file in the destination's directory, fix its mode, then rename() it over the old file; on one file system that swap is atomic. Across file systems PHP falls back to copy-and-delete, which readers can catch half-written.

solid answer

~40 s

Writing in place with `file_put_contents($target, $data)` truncates first, so a reader can see an empty or partial file. The safe pattern is: `tempnam(dirname($target), 'tmp')` to create a uniquely named file next to the target; write the full content; optionally `fflush()` and `fsync()` (PHP 8.1+) for durability; `chmod()` it, because `tempnam()` creates files with mode `0600`; then `rename($tmp, $target)`. On one file system `rename()` is a single system call that swaps the directory entry, so readers get either the old file or the new one. It stops being atomic when source and target are on different file systems: php-src then copies the data into the target path and unlinks the source, which is visible half-way. `copy()` never gives this guarantee, and on Windows an existing target must be writable.

go deeper

for a junior

Recall the pattern: write to a temp file in the same directory, then rename() it over the target.

for a middle

Explain why rename() is atomic on one file system, why tempnam() files need chmod(), and why copy() gives no such guarantee.

for a senior

Show you spot the cross-file-system fallback, check where tempnam() really created the file, and fsync() before the swap when durability matters.

for a principal

Choose between file swaps, symlink swaps and moving the data to a store with transactional updates, based on how many readers and hosts depend on it.

## The problem with writing in place A generated configuration, a price list or a rendered page is read constantly while a job refreshes it. Writing it in place is not safe: - `file_put_contents($target, $data)` opens with truncation, so for a moment the file is **empty**, then **partial**. - `file_put_contents($target, $data, LOCK_EX)` only helps readers that also take a shared lock; plain `file_get_contents()` and `include` take none. - If the writer crashes half-way, the damaged file stays. ## Write, then swap The standard fix is to never modify the live file. Build the new version beside it and swap names: ```php <?php declare(strict_types=1); function replaceAtomically(string $target, string $data): void { $tmp = tempnam(dirname($target), '.tmp-'); if ($tmp === false || dirname($tmp) !== dirname($target)) { throw new RuntimeException('temp file not created next to target'); } try { $h = fopen($tmp, 'w'); if ($h === false || fwrite($h, $data) !== strlen($data)) { throw new RuntimeException('short write'); } fflush($h); fsync($h); fclose($h); chmod($tmp, 0o644); if (!rename($tmp, $target)) { throw new RuntimeException('rename failed'); } } finally { if (is_file($tmp)) { unlink($tmp); } } } ``` Step by step: 1. **`tempnam(dirname($target), …)`** creates a uniquely named file in the **same directory** as the target, so it is on the same file system. The `dirname()` check catches `tempnam()`'s silent fallback to the system temp directory, which it signals only with an `E_NOTICE`. 2. **Write everything** and check the byte count. 3. **`fflush()` and `fsync()`** push the data to storage before the swap, so a crash right after the rename does not leave an empty new file. `fsync()` exists since PHP 8.1. 4. **`chmod()`**, because `tempnam()` creates files with mode `0600`. Skip this and the web server or other readers get *Permission denied* on the fresh file. 5. **`rename($tmp, $target)`** replaces the target in one step. 6. The `finally` removes the temp file if anything failed before the rename. ## Why rename() is atomic, and when it is not On a local file system, `rename()` maps to the `rename(2)` system call, which replaces the directory entry in one operation. A process that opens the path gets either the old inode or the new one, never a mixture; a process that already had the old file open keeps reading the old content. PHP then clears its stat and realpath caches. The guarantee has limits: | Situation | What PHP does | Atomic? | |---|---|---| | same file system, file over file | `rename(2)` | yes | | different file systems (`EXDEV`) | copies the data into the **target path**, copies owner and mode where it can, then unlinks the source | **no**: readers can see the partial copy | | a file renamed over an existing directory | fails with an `E_WARNING` | no swap happens | | Windows, existing target | replaces it only if the target is writable, otherwise `E_WARNING` | platform-dependent | | `copy($src, $target)` | writes into the target path directly | never | The cross-file-system fallback is the quiet trap: `rename()` still returns `true`, so nothing tells you the swap was not atomic. Common causes are a temp directory on a separate mount (a `tmpfs` `/tmp`) or a container volume mounted over the target directory. Creating the temp file next to the target avoids it. ## Checklist 1. Temp file created with `tempnam()` in the target's directory, and its location verified. 2. Full content written and the byte count checked. 3. `fflush()` and `fsync()` when a crash must not leave an empty file. 4. `chmod()` to the mode readers need, since the temp file starts at `0600`. 5. `rename()` over the target, and the temp file removed on every failure path. ## Related points interviewers probe - **Readers holding the old file** are unaffected. That is a feature for long reads and a surprise for code that expects to see the change through an already open handle. - **OPcache** keeps serving a replaced PHP file until it revalidates; atomic replacement fixes torn reads, not cache staleness. - **Directories** cannot be renamed over a non-empty directory; the operating system refuses, and php-src passes the call straight to it. That is why deploys usually swap a symlink instead. - **Permissions and ownership** of the new file are whatever you give the temp file; the old file's mode is not inherited.

  • Why does the example compare dirname($tmp) with dirname($target)?
    `tempnam()` does not fail when the directory is missing or not writable; it creates the file in the system temp directory and only emits an `E_NOTICE`. If that directory is on another file system, the later `rename()` silently degrades to copy-and-unlink. Checking the directory turns that quiet fallback into an exception.
  • A process already has the old file open when rename() replaces it; what does it read?
    It keeps reading the old content. The open handle refers to the old inode, which the operating system keeps alive until the last handle closes, even though the name now points to the new file. Only processes that open the path after the rename see the new version.
  • Is file_put_contents($target, $data, LOCK_EX) an alternative to the rename pattern?
    Only for cooperating readers. It opens without truncating, takes an exclusive lock, then truncates and writes, so readers that take `LOCK_SH` wait. Readers that do not lock, such as `file_get_contents()`, can still see the empty or partial file. The rename pattern needs no cooperation from readers.

Replacing a file with rename() is like swapping the sign on a shop door: the new shop is fully stocked behind a different door, and customers see either the old sign or the new one. Copying across file systems is restocking the shelves while customers are already inside.

saying these in an interview costs you the question

  • rename() is always atomic, even across two mounted file systems.
  • copy() followed by unlink() is as safe as rename() for replacing a file.
  • A file created by tempnam() is readable by the web server without chmod().
  • rename() returns false when it has to fall back to copying across file systems.
  • Processes that already opened the old file start reading the new content.