Skip to content
Select theme

How to write a file atomically

Short answer: Write the new contents to a temporary file in the same directory, then fs::rename it over the target. The rename replaces the original file in one operation, so a reader sees either the old file or the new one — never a half-written file, and never a truncated one.

From a fresh project — cargo new atomic-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else on this page depends on it. Each block writes the file it then replaces, so it runs on its own and in any order. Paths are relative to the directory you run from, which under cargo run is the project root.

fs::write is one call, and inside that call the file is emptied before the first byte of the new contents arrives. Kill the process halfway through and the file keeps exactly what reached the disk:

before: 32 bytes
after: 17 bytes
"a partial replace"

Thirty-two bytes became seventeen, and a reader that opened the file mid-write saw a state that never existed as a complete file. For a file nobody reads until it is finished, that is fine. For a config file or a save game — anything a reader can catch in flight — it isn’t.

The fix is to stop writing to the path a reader uses. Write the new contents to a temporary file beside it, then fs::rename that file over the target:

before: "the original, complete contents\n"
after: "a complete replacement\n"
23 bytes
temp file left behind: false

The rename is the commit point. A reader that opens the file before it sees the old contents; one that opens it after sees the new ones — the before: and after: lines above are exactly those two views. A crash before the rename leaves the original untouched and a stray .tmp behind, which is the safe way to fail. sync_all pushes the bytes to the disk before the rename happens; without it the rename can be visible while the contents are still in cache.

  • The temp file goes in the same directory as the target, not in /tmp. fs::rename doesn’t work across filesystems — its own documentation says so — and the same-directory rename is the one that swaps the file in place.
  • A fixed .tmp name collides when two writers run at once. Put the process id in the name — format!("{path}.{}.tmp", std::process::id()) — or use a counter.
  • Errors surface before the commit. If the write or the rename fails, the target still holds the previous contents, so a failed save leaves the last good one intact.

Atomicity covers the contents of one file. Choosing between fs::copy and fs::rename when data moves around is a different problem, and it gets its own page next in this chapter.