Plate 17
Atomic Rename vs Overwrite: Config Write Lab
Hands-on atomic rename vs overwrite lab: tempfile+os.replace vs truncate-write with/without fsync. Real ops/s on localhost; crash-safety limits labeled.
Aditya Challa6 min read
On this page
- Intro — what this post promises
- Patterns under test
- Lab topology
- Lead table — 4 KiB (p50)
- Other sizes
- Crash-safety — what we are not claiming
- How to read these numbers
- Pitfalls we hit (or avoided)
- Practical checklist
- Methodology footnote
- Versions / environment
- Why same-directory temps matter
- Readers during replace
- Ratio sheet (med 4 KiB ops/s ÷ overwrite)
- Verdict
Intro — what this post promises
Updating a config file with open(path, "w") can leave a torn file if the process dies mid-write. The usual fix is write a temp file in the same directory, then os.replace onto the target. How much throughput does that cost — and what happens when you add fsync?
This is a hands-on lab with measured numbers:
- Truncate-overwrite vs temp +
os.replacefor 64 B / 4 KiB / 64 KiB payloads. - Same arms with file
fsyncafter write. - Durable-ish pattern: fsync file + replace + fsync directory.
- Explicit note: no crash/power-fail simulation.
Related links:
- fsync vs fdatasync localhost lab
- SQLite WAL vs DELETE journal localhost lab
- posix fadvise sequential vs random localhost lab
- json vs orjson vs msgpack localhost lab
- Why your average latency graph is lying (p50 / p95 / p99)
Lab honesty (1 Oct 2026 IST): Shared Linux lab box; Python 3.13.5; files under /tmp on overlay. Affiliates: 0. Throughput ≠ proven crash recovery.
Verdict up front: for 4 KiB, replace costs 18%~~ vs overwrite (13580 vs 16566 ops/s). fsync is the cliff (~~3–5× slower). File+dir fsync rename landed ~2240 ops/s.
Patterns under test
| Pattern | What it does | Readers see |
|---|---|---|
| overwrite | open(wb) truncate + write | may see partial on crash |
| replace | write temp → os.replace | old or new complete (same FS) |
| + fsync file | durability of data bytes | still may miss dir entry without dir fsync |
| + fsync dir | durable rename cookbook | still not a formal crash proof here |
Related links:
Lab topology
Script: lab-evidence/42-atomic-rename/results/run_lab.py.
Lead table — 4 KiB (p50)
| Arm | ops/s | µs/op | vs overwrite |
|---|---|---|---|
| overwrite | 16566 | 60.4 | 1.00× |
| replace | 13580 | 73.6 | 0.82× |
| overwrite + fsync | 5029 | 199 | 0.30× |
| replace + fsync | 3376 | 296 | 0.20× |
| replace + fsync file+dir | 2240 | 446 | 0.14× |
Atomicity without fsync is cheap here (~18%). Durability is expensive — same story family as the fsync lab.
Related links:
Other sizes
64 B: overwrite 16377 ops/s; replace 13698 (~0.84×); overwrite+fsync 4929; replace+fsync 3377.
64 KiB: overwrite 5234; replace 7558 (~1.44× faster replace!). Writing a new temp + rename beat truncating and rewriting the large file on this run — worth re-checking on your FS, but it shows replace is not always the slower path for big blobs.
Crash-safety — what we are not claiming
We did not pull power, kill -9 mid-write, or run dm-flakey. The durable rename pattern is the industry cookbook (fsync data file, rename/replace, fsync directory). This lab times that cookbook; it does not prove your disk honors flush.
Related links:
How to read these numbers
- Prefer replace for config/state files readers must never see half-written.
- Budget ~20% throughput on small writes here without fsync — or gain on large rewrites.
- If you need durability across power loss, pay fsync (and likely dir fsync).
- Same-filesystem
os.replaceis required for atomicity; cross-device becomes copy+unlink.
Pitfalls we hit (or avoided)
- Calling replace “free” — ~18% on 4 KiB.
- Calling replace “always slower” — 64 KiB flipped.
- fsync file only and declaring durable rename done — we timed dir fsync too.
- Simulating crash with a blog claim — labeled out of scope.
- Ignoring reader concurrency — atomicity helps; still need locking if two writers.
Practical checklist
- Config updates: temp in same directory +
os.replace. - Need power-fail durability: fsync file (+ dir) and measure.
- Don’t fsync every keystroke unless RPO demands it.
- Keep overwrite for scratch/temp artifacts you can rebuild.
- Pair with the fsync lab numbers when capacity planning.
Related links:
- posix fadvise sequential vs random localhost lab
- sha256 vs blake2 xxhash localhost lab
- zstd vs gzip vs lz4 compression localhost lab
- json vs orjson vs msgpack localhost lab
Methodology footnote
Each op fully writes the payload. Replace arms use unique temp names then os.replace. Directory fsync uses os.open(dir, O_RDONLY) + os.fsync(dir_fd). Ops/s = n / p50_batch_seconds.
Versions / environment
- Python 3.13.5
- Workdir under
/tmpon overlay/virtio lab VM
Writable config files are where torn writes hurt humans. Pay the replace tax; add fsync only when the durability contract requires it.
Why same-directory temps matter
os.replace is atomic only on the same filesystem. Writing to /tmp/foo and replacing onto /var/lib/app/config.json can silently become “copy + unlink” semantics across mounts — or fail. This lab kept temps beside the target under /tmp. Production configs usually live on the data volume; create config.json.tmp next to config.json, not on an unrelated tmpfs, unless you intentionally accept non-atomic publish.
Readers during replace
After a successful replace, a reader opening the path sees either the previous complete file or the new complete file — not a mix of both contents from a single truncated write. Readers that already hold an open FD keep the old inode (classic Linux behavior). That is often what you want for long-lived workers; document whether they must reopen to pick up config.
Batch exporters that rewrite large snapshots may find replace faster than overwrite (our 64 KiB case). Profile your size; do not assume the 4 KiB ranking.
Ratio sheet (med 4 KiB ops/s ÷ overwrite)
| Arm | Ratio |
|---|---|
| overwrite | 1.00 |
| replace | 0.82 |
| overwrite+fsync | 0.30 |
| replace+fsync | 0.20 |
| replace+fsync file+dir | 0.14 |
If you currently overwrite without fsync, switching to replace is the cheap correctness upgrade. If you already fsync every overwrite, moving to replace+fsync (or +dir) is mostly about atomic visibility, not a new performance class — you already paid the barrier.
Compare absolute µs/op to the fsync lab’s per-record sync costs: both say “barriers dominate metadata tricks.”
Ship replace by default for user-visible state files; measure before adding fsync-on-every-write in a tight loop.
Verdict
On this box, 4 KiB atomic replace ran at ~13580 ops/s versus overwrite ~16566 (~0.82×). Adding fsync dropped replace to ~3376 ops/s; fsync file+dir to ~2240. Atomicity is affordable; durability dominates — and we still did not simulate a crash.
Evidence: lab-evidence/42-atomic-rename/results/. Affiliates: 0.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5 on /tmp overlay. med 4KiB: overwrite 16566 ops/s (60 us); replace 13580 (~0.82x); overwrite+fsync 5029 (~0.30x); replace+fsync 3376 (~0.20x); replace+fsync file+dir 2240 (~0.14x). small 64B similar. large 64KiB replace 7558 vs overwrite 5234 without fsync. No crash simulation. Affiliates: 0. Evidence: lab-evidence/42-atomic-rename/.
Related links
Plate 17
platform vs os.uname Inventory: Localhost Lab
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026
Plate 75
uuid.uuid4 vs uuid.uuid1: Localhost Lab
Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026
Plate 50
signal vs threading.Event Wakeup: Localhost Lab
Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026