Crash Consistency: fsync(), rename(), and Durable Writes You Can Trust
This article covers Crash Consistency: fsync(), rename(), and Durable Writes You Can Trust. Power loss and crashes turn "it worked" into "it corrupted". Learn what fsync guarantees, why rename is special, and how to implement...
If you write a file and your program prints “saved!”, you still might lose data after a crash.
Crash consistency is about making sure that after an unexpected reboot/power loss, the disk contains either:
- the old valid state, or
- the new valid state
…not something half-written.
1) The main idea: separate "write" from "durable"
On modern OSes, write() typically copies into the page cache. It does not mean the data reached stable storage.
Durability usually requires:
fsync(fd)(orfdatasync(fd)) to force data to disk- sometimes also syncing the directory that contains your file
2) Why rename() is special
On POSIX filesystems, rename("tmp", "final") is typically atomic:
- after crash, name points to either old file or new file
- you won't see a filename that is half-updated
But atomic name update is not the same as durable data. You still must fsync correctly.
3) The standard pattern: write temp + fsync + rename + fsync dir
Crash-consistent update (high-level):
- Create
file.tmpin same directory - Write full content to temp
fsync(temp_fd)rename(file.tmp, file)fsync(dir_fd)to persist directory entry update
4) C: crash-consistent save
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static int write_all(int fd, const void *buf, size_t n) {
const unsigned char *p = (const unsigned char*)buf;
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, p + off, n - off);
if (w < 0) {
if (errno == EINTR) continue;
return -1;
}
off += (size_t)w;
}
return 0;
}
int atomic_save(const char *path, const void *data, size_t len) {
// temp file path (simple approach; robust code should avoid collisions)
char tmp[4096];
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0) return -1;
if (write_all(fd, data, len) != 0) { close(fd); return -1; }
if (fsync(fd) != 0) { close(fd); return -1; }
if (close(fd) != 0) return -1;
if (rename(tmp, path) != 0) return -1;
// fsync directory to persist rename
int dfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dfd >= 0) {
(void)fsync(dfd);
close(dfd);
}
return 0;
}
Notes:
- The directory fsync should target the directory of
path, not necessarily.. - For a real implementation, open the directory fd and use
openat/renameat.
5) Zig: atomic save using std.fs
const std = @import("std");
pub fn atomicSave(path: []const u8, data: []const u8) !void {
const cwd = std.fs.cwd();
var tmp_name_buf: [512]u8 = undefined;
const tmp = try std.fmt.bufPrint(&tmp_name_buf, "{s}.tmp", .{path});
var f = try cwd.createFile(tmp, .{ .truncate = true });
defer f.close();
try f.writeAll(data);
try f.sync();
try cwd.rename(tmp, path);
// Best effort: sync directory entry updates.
var dir = try cwd.openDir(".", .{});
defer dir.close();
try dir.sync();
}
6) Rust: atomic save pattern
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
fn atomic_save(path: &Path, data: &[u8]) -> io::Result<()> {
let tmp = path.with_extension("tmp");
{
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)?;
f.write_all(data)?;
f.sync_all()?;
}
fs::rename(&tmp, path)?;
// Sync the containing directory
if let Some(parent) = path.parent() {
let dir = File::open(parent)?;
dir.sync_all()?;
}
Ok(())
}
7) fsync vs fdatasync
fsync: sync data + metadatafdatasync: sync data + minimal metadata needed for data
If you update only file contents, fdatasync can be enough (but be careful: changing file size typically involves metadata too).
References
fsync(2): https://man7.org/linux/man-pages/man2/fsync.2.htmlfdatasync(2): https://man7.org/linux/man-pages/man2/fdatasync.2.htmlrename(2): https://man7.org/linux/man-pages/man2/rename.2.html- SQLite atomic commit / durability notes (excellent): https://www.sqlite.org/atomiccommit.html
- PostgreSQL WAL / durability concepts: https://www.postgresql.org/docs/current/wal-intro.html