After power loss, assume only a narrow contract: on Linux, rename() can make a destination-name replacement atomic for concurrent access, but that is a visibility property, not by itself a reboot-durability guarantee. Successful writes may still be absent after restart until you flush them, and append paths should be designed as if a crash could leave a partial or otherwise invalid tail record. Design around explicit commit points and startup repair, not around the hope that the filesystem preserved your intent.
What does the operating system actually promise?
Treat visibility and durability as separate. On Linux, rename() says that if newpath already exists, it is atomically replaced so another process will not observe newpath missing during the replacement. That is why replace-via-temp-file is the right starting pattern.
But fsync() documents an important limit: flushing a file does not by itself guarantee the containing directory entry is durable. The manual states that fsync() on the file “does not necessarily ensure that the entry in the directory containing the file has also reached disk” and that an explicit fsync() on the directory is needed for that. If your crash-safety boundary is “the new filename must survive reboot,” do not treat the rename() call alone as that boundary.
That leads to the application contract: after a crash, code should be prepared for old state, new state, or leftover temporary artifacts depending on what reached stable storage and on filesystem behavior. Do not assume the rename alone defines the reboot-visible result, and do not assume your last successful syscall sequence fully reached stable storage unless you flushed both the file data you care about and the directory change that makes it reachable.
How should you replace a file safely?
For whole-file replacement, use this sequence in the target directory:
- Create
state.tmpin the same directory asstate. - Write the complete new contents.
fsync(state.tmp)orfdatasync(state.tmp).rename(state.tmp, state).- Open the parent directory and
fsync(dirfd).
Worked example: suppose state stores a JSON snapshot. If power fails after step 3 but before step 4, startup may find the old state plus state.tmp. After step 4, do not treat the name change as crash-durable until step 5 completes, because fsync(2) says file fsync() alone does not necessarily persist the containing directory entry. Recovery should therefore validate state and any recognized temp file according to an application-defined rule—for example, state is authoritative unless you maintain a separate commit marker—then clean up unused temp files. Do not guess intent from timestamps.
How should you handle append paths?
Assume append is not a transaction. SQLite’s atomic commit documentation is useful here because it explains that SQLite does not, by default, assume sector writes are atomic and instead builds recovery around detectable boundaries and repair. For an append-only log, design as if a crash could leave a partial or otherwise invalid tail record, and make each record self-validating: length, sequence number, payload, checksum.
Then define the commit boundary you are willing to pay for: flush every record, every batch, or every interval. On startup, scan forward, verify each record, stop at the first impossible length or bad checksum, and truncate back to the last valid offset. If your caller can retry after an uncertain crash point, use sequence numbers so replay is idempotent and duplicate appends are harmless.
What should startup recovery verify?
Use this checklist:
- What is authoritative: snapshot, log, or snapshot plus log?
- Which syscall marks durability for your design?
- Can you detect incomplete data unambiguously?
- Can you roll forward or truncate back to a known-good boundary?
- Have you tested this exact sequence on each target filesystem and mount setup?
Linux syscall documentation is necessary, but not a complete portability contract. Filesystem behavior and configuration still matter, so document the sequence your code depends on and verify it with crash-injection tests on the real deployment stack.
Do I always need to fsync() the directory after rename()?
If the directory entry created or replaced by rename() must be persisted as part of your crash-recovery contract, fsync(2) says you need an explicit fsync() on the containing directory; fsync() on the file alone does not necessarily ensure that directory entry has reached disk.
Is O_APPEND enough for crash-safe logging?
No. It helps place writes at end-of-file, but it does not by itself give record integrity, checksums, or recovery rules for an invalid tail after a crash.
Next step: write your intended durable commit as a five-step syscall sequence in code comments, then add a crash test that interrupts execution after every step and proves startup can recover.
Reviewed: 2026-09-06
Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.
Read our editorial approach ↗