Atomic writes: temp file then rename

Never edit the live file in place — build the new version off to the side, then swap it in with one all-or-nothing rename.

The idea

When you overwrite a file directly, the new bytes go down a little at a time. If the machine loses power halfway through, the file is left as a mangled mix of old and new — neither version is intact, and a reader sees garbage.

The fix is to never touch the live file while you build its replacement. Write the complete new version to a temporary file alongside it, make sure it's safely on disk, then rename the temp file over the destination. On POSIX systems rename() is atomic, so a reader always sees either the old whole file or the new whole file — never a torn one.

See it work

Press play, or step through. Try firing “Crash now” mid-write.

How it works

Write the replacement to a temp file in the same directory as the destination (so the final rename stays on one filesystem), flush it all the way to disk with fsync, then call os.replace to swap it in. os.replace maps to an atomic rename, so the swap happens as a single indivisible step.

import os, tempfile

def atomic_write(path, data):
    d = os.path.dirname(os.path.abspath(path))
    # Temp file MUST live in the same directory / filesystem
    # as the destination, or the rename can't be atomic.
    fd, tmp = tempfile.mkstemp(dir=d, prefix=".tmp-")
    try:
        with os.fdopen(fd, "w") as f:
            f.write(data)
            f.flush()
            os.fsync(f.fileno())   # force bytes to disk before renaming
        os.replace(tmp, path)      # atomic rename over the destination
        # Optional: fsync the directory so the rename itself survives a crash.
        dirfd = os.open(d, os.O_RDONLY)
        try:
            os.fsync(dirfd)
        finally:
            os.close(dirfd)
    except BaseException:
        os.unlink(tmp)             # clean up the temp file on failure
        raise

A reader opening path at any instant sees the old complete file before the rename and the new complete file after it. There is no moment where the file is half-written.

Cost / trade-offs

What it costsWhat you get
Extra temp space — briefly two full copies of the file on disk. A crash mid-write can never corrupt the live file.
An extra fsync on every save (a real durability cost — fsync is slow). The new bytes are actually on disk before they replace the old ones.
Atomic only within the same filesystem — a cross-device rename degrades into a copy. Same-dir temp keeps the rename a true atomic metadata swap.
Without the fsync first, the rename can be reordered ahead of the data on some setups. fsync-then-rename guarantees the data is durable before the pointer moves.

Watch out for

Worked example

Picture a settings saver that must update config.json. It writes the new JSON to config.json.tmp in the same folder, calls fsync on it, then runs os.replace("config.json.tmp", "config.json").

Now suppose the power dies at the worst possible instant. If it dies during the temp write, config.json was never touched — the app boots with the old, intact config and the leftover .tmp is just discarded. If it dies right at the rename, the rename either fully happened or fully didn’t, so the app sees the old config or the new config — never a half-parsed file that crashes the loader. Either way, the user’s settings survive.

Check yourself

1. You write the temp file to /tmp/data.json.tmp while the real file lives at /var/app/data.json, then call rename. Is the swap still atomic?

2. You skip fsync on the temp file and rename immediately. What can go wrong on a crash?