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.
What a crash leaves behind
Section titled “What a crash leaves behind”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:
use std::fs::{self, File};use std::io::Write;
fn main() -> std::io::Result<()> { fs::write("data.txt", "the original, complete contents\n")?; println!("before: {} bytes", fs::metadata("data.txt")?.len());
// The same write again, killed after the first half. // File::create empties the file before anything is written. let mut file = File::create("data.txt")?; file.write_all(b"a partial replace")?; // The process dies here.
println!("after: {} bytes", fs::metadata("data.txt")?.len()); println!("{:?}", fs::read_to_string("data.txt")?); Ok(())}before: 32 bytesafter: 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.
Write to a temp file, then rename
Section titled “Write to a temp file, then rename”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:
use std::fs::{self, File};use std::io::{self, Write};
fn write_atomic(path: &str, contents: &[u8]) -> io::Result<()> { // The temp file sits next to the target, so the rename stays on one filesystem. let tmp = format!("{path}.tmp"); let mut file = File::create(&tmp)?; file.write_all(contents)?; file.sync_all()?; drop(file); // closed before the rename fs::rename(&tmp, path)?; Ok(())}
fn main() -> io::Result<()> { fs::write("data.txt", "the original, complete contents\n")?; println!("before: {:?}", fs::read_to_string("data.txt")?); write_atomic("data.txt", b"a complete replacement\n")?; println!("after: {:?}", fs::read_to_string("data.txt")?); println!("{} bytes", fs::metadata("data.txt")?.len()); println!("temp file left behind: {}", fs::metadata("data.txt.tmp").is_ok()); Ok(())}before: "the original, complete contents\n"after: "a complete replacement\n"23 bytestemp file left behind: falseThe 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 details that matter
Section titled “The details that matter”- The temp file goes in the same directory as the target, not in
/tmp.fs::renamedoesn’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
.tmpname 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.
Related
Section titled “Related”- How to write to a file — the one-call truncating write, and when it is the right one
- How to append to a file without overwriting it — the other half of the crash problem
- Reading and writing files — the rest of the chapter