Journaling and Crash Consistency
Imagine a file system update that must touch three different disk blocks. These are an inode, a bitmap, and a data block. Power fails after only one write lands. The on-disk structures are now inconsistent. The Inodes and File Metadata the file system trusts may point to garbage. It may also leak space forever. This is the crash-consistency problem. Every real file system must solve it. Disks and machines fail at the worst possible moment. This always happens mid-update, never conveniently between transactions. The naive fix worked for decades but does not scale. It involves scanning the entire disk after every reboot. Anything that looks wrong gets repaired. A file-system checker (`fsck`) on a multi-terabyte volume can take hours. This is an outage no modern server or phone can tolerate. The insight that solves this cleanly borrows an idea from database systems. That idea is write-ahead logging. Before modifying the real, in-place structures, describe the intended changes in a separate log. Only after the log is safely on disk do you apply the changes. Then they reach their final locations. If a crash happens, replay the log and finish what was promised. If the log itself is incomplete, discard it. Nothing was ever half-done. This article develops that idea rigorously. It builds from the naive `fsck` baseline. Then it moves through write-ahead journaling as used in ext3/ext4, NTFS, and XFS. This includes the ordered-versus-full-data-journaling tradeoff. From there it proceeds to copy-on-write file systems such as ZFS and Btrfs. These sidestep in-place updates entirely. Along the way we make precise what "consistent" means. We also prove why each mechanism actually restores it. By the end you will be able to trace, block by block, what happens to a journal. You will also trace a file system's metadata across a crash at any point in an update. You will explain why journaling reduces recovery time from O(disk size) to O(log size). You will compare journaling to copy-on-write on correctness, performance, and write amplification. This groundwork also sets up how systems like RAID and Redundant Storage compose with crash consistency. Together they protect against whole-disk failures, not just partial writes.
- A crash mid-update can leave file-system metadata in an inconsistent state (orphaned inodes, leaked blocks, corrupted directories).
- The classical fix, `fsck`, scans the whole disk to find and repair inconsistencies — correct but O(disk size), too slow for modern storage.
- Write-ahead journaling logs intended changes before applying them in place, so recovery only needs to replay/discard the log, in O(log size).
- Metadata journaling (ordered mode) journals metadata only and forces data writes first, giving strong guarantees at low overhead — ext4's default.
- Full data journaling writes data through the log too, giving the strongest guarantees but roughly doubling write traffic.
- Copy-on-write file systems (ZFS, Btrfs) never overwrite live data in place; an atomic root-pointer update makes the whole tree consistent by construction, no journal replay needed.
Never overwrite the only copy of something before its replacement is durably recorded — write the plan down first, or write a new copy and atomically switch pointers, so a crash can always be resolved without scanning the whole disk.
The idea
A file-system update — creating a file, appending data, truncating — typically requires modifying several on-disk structures that must remain mutually consistent: an inode's size and block pointers, a free-space bitmap or extent tree, a directory entry, and the data blocks themselves. These writes physically land on disk one at a time (or a few at a time, subject to hardware write ordering), never all atomically. If the machine loses power, or the OS crashes, between writes, the disk is left in a state that corresponds to no single point in the intended sequence of operations — some updates happened, others did not. This is the crash-consistency problem: after an arbitrary crash, restore the on-disk image to a state that is consistent, meaning it satisfies the file system's own invariants (every allocated block is referenced by exactly one live inode, every bitmap bit matches actual allocation, every directory entry points to a valid inode, and so on). Two broad strategies solve this. Write-ahead logging (journaling) preserves in-place updates but adds a redundant, sequential log recording the intent of a transaction before it is applied; after a crash, the log is consulted to either finish or fully discard incomplete transactions, restoring a state equivalent to "transaction happened" or "transaction never happened" — this atomicity property is what database theory calls the A in ACID, applied to file-system metadata. Copy-on-write (CoW) instead never overwrites live, referenced data: every update writes new blocks and a new metadata tree pointing to them, culminating in a single atomic write of a root pointer (a superblock update) that switches the entire file system from the old, still-fully-intact tree to the new one. Both approaches guarantee that a crash can only ever be observed at a transaction boundary — never mid-transaction — which is precisely the consistency guarantee `fsck` had to reconstruct at great cost by inspecting every structure on disk.
How it works
Baseline: `fsck` (file system check). Before journaling, systems like the classic Unix File System (FFS) relied on `fsck` to run at boot after an unclean shutdown. It performs a global consistency pass: ``` FSCK(): scan all inodes; build a table of which blocks each live inode claims scan the free-space bitmap for each block: if claimed by >1 inode: flag cross-link, allocate a copy to one, or truncate if claimed by an inode but marked free in bitmap: fix bitmap if marked allocated in bitmap but claimed by no inode: mark free (leaked block) for each directory: verify every entry references a valid, allocated inode verify link counts on inodes match number of directory entries referencing them for each inode with link count 0 but still referenced: fix reference, or free it write corrected bitmaps / inodes / directories back to disk ``` This works — it can detect and repair every class of pointer, count, and allocation inconsistency — but it must touch metadata proportional to the size of the entire volume, not the size of the interrupted update. On a multi-terabyte disk this is minutes to hours, an unacceptable outage, and it cannot recover data that was mid-write, only restore metadata consistency.
Write-ahead journaling. The fix is to record, before touching the real (in-place) structures, a compact description of the transaction in a dedicated log region (the journal), and only mark the transaction as "committed" once that description is durably on disk. Journals are typically implemented as a circular buffer of fixed size at a known disk location. A generic journaling protocol proceeds in phases: ``` BEGIN_TRANSACTION(TID): allocate a transaction record in the journal
# Journal write phase WRITE_JOURNAL(TID, blocks): write a transaction-begin record (TID, list of target block numbers) write the contents of each modified block into the journal (or, in ordered mode, only metadata blocks — see below) write a transaction-commit record (TID), tagged with a checksum force all of the above to disk with a barrier / flush (commit record must be the LAST thing to become durable)
# Checkpoint phase — only after commit record is durably on disk CHECKPOINT(TID): write each journaled block to its real, in-place location on disk once all writes succeed, mark the transaction as "free" in the journal (its journal space may now be reused) RECOVER(): # run at reboot, replaces fsck's full scan scan the journal from its oldest live position for each transaction record found: if a valid commit record exists for it (checksum matches): replay: re-write its logged blocks to their in-place locations else: discard: skip it entirely, as if it never began resume normal operation ``` The crucial correctness property is the ordering constraint: the commit record must reach stable storage strictly after all the transaction's logged data, and in-place ("checkpoint") writes must happen strictly after the commit record is durable. This ordering, not the journal's mere existence, is what makes recovery well-defined; file systems enforce it with write barriers or explicit cache-flush commands (since disks and SSDs may reorder or buffer writes internally).
Metadata (ordered) journaling vs. full data journaling. Journaling every byte of every write (full data journaling, `journal` mode in ext3/ext4) gives the strongest guarantee — both data and metadata are transactionally consistent — but every block is written twice: once to the journal, once at checkpoint, roughly doubling write traffic and halving effective bandwidth for write-heavy workloads.
Ordered journaling (`ordered` mode, ext3/ext4's default) journals only metadata (inode updates, bitmap changes, directory entries), but adds one ordering rule: all data blocks belonging to a transaction must be written to their final location before the corresponding metadata's commit record is written. This avoids double-writing data while still preventing the classic "garbage data" bug: without this rule, a crash after the metadata commits but before data is written could expose an inode pointing at a block full of previously-deleted, unrelated data (a security and correctness hazard) or, in the case of appends, a block full of uninitialized disk content. Ordered mode guarantees that if metadata says "this block belongs to this file," the block's content is at least the new content, or, if the transaction never committed, the old content — never leftover garbage from something else.
Writeback journaling (the third, weakest mode) journals only metadata without ordering it after data. It is fastest but permits stale/garbage data to appear after crash recovery, since metadata can commit and be replayed before the corresponding data write ever reaches disk. It preserves metadata consistency (no more `fsck` needed for structure) but not data consistency.
Copy-on-write. CoW file systems (ZFS, Btrfs, WAFL) take a structurally different approach: they never modify a live block in place. An update to a file allocates new blocks for the changed data, then allocates new copies of every metadata block on the path from that data block up to the root (the classic technique is a copy-on-write Merkle-like tree, sometimes literally a B-tree of block checksums). The very last step is a single atomic write of the filesystem's root pointer (in a superblock or uberblock) to point at the new tree root instead of the old one. ``` CoW_UPDATE(file, new_data_block): new_data = allocate_block(); write(new_data, new_data_block) leaf = copy_of(inode_block_pointing_to(old_data_block)) leaf.pointer = new_data new_leaf = allocate_block(); write(new_leaf, leaf)
# propagate the copy upward to the root for each ancestor level up to the root: parent_copy = copy_of(parent) parent_copy.pointer_to_child =
write(new_block_for(parent_copy), parent_copy)# atomic switch: this single write is the only mutation of a "live" pointer write_superblock(new_root_pointer) # old tree remains fully intact until this instant ``` Because the old tree is untouched until the very last, single-sector superblock write — and a single sector write is atomic on virtually all storage hardware — a crash at any point before that write leaves the old, fully consistent tree as the visible state; a crash after it leaves the new, fully consistent tree as the visible state. There is no window of partial application to detect or repair, and hence no replay log is needed: recovery is just "look at whichever root pointer is valid" (often chosen via a checksum or a generation-number comparison between two redundantly stored superblocks).
Complexity & correctness
`fsck` complexity. A full scan must read every inode, every bitmap block, and every directory to cross-validate structures, so its time is where is the number of blocks/inodes on the volume, independent of how much was actually being modified at crash time. Space is in the worst case (in-memory tables mapping blocks to owning inodes for cross-link detection), though streaming implementations reduce this with multiple passes. This is correct — it is a full re-derivation of every invariant from scratch — but asymptotically wasteful relative to the size of the actual interrupted operation, which is typically to blocks for a small transaction.
Journaling recovery complexity. Recovery scans only the journal region, of fixed, administrator-configured size (typically tens to hundreds of MB), replaying at most the transactions still resident in it: time is , and in practice , since old, checkpointed transactions are pruned from the journal continuously during normal operation. Because for any real volume, recovery time becomes independent of total disk size — this is the entire point, and it is what makes journaling recovery a constant-ish, sub-second-to-seconds operation rather than an hours-long scan.
Correctness argument for journaling. Define a transaction as committed if and only if its commit record, with a valid checksum covering all its logged blocks, is present in the journal on disk. The protocol enforces two ordering invariants via write barriers: 1. (Log-before-commit) All of 's data/metadata blocks are durable in the journal before 's commit record is written. 2. (Commit-before-checkpoint) 's commit record is durable before any of 's blocks are written to their in-place locations. Claim: after recovery, the on-disk state is exactly as if either all of 's writes happened atomically, or none did — for every transaction , regardless of when the crash occurred. Proof sketch. Suppose crash occurs before 's commit record is durable. By invariant 1's contrapositive, no in-place checkpoint write for could have happened yet, because checkpoint writes only start after commit — so the disk still shows the pre- state for every block would have touched; recovery finds no valid commit record (checksum fails or record absent) and correctly discards , matching the pre-state. Now suppose the commit record is durable at crash time. Then, by invariant 1, all of 's logged block images are already safely in the journal. Recovery finds the valid commit record and replays every logged block to its in-place location; whether zero, some, or all of the "real" checkpoint writes had already succeeded before the crash is irrelevant, because replay is idempotent — rewriting a block that already holds the correct value is a no-op in effect. So after replay, every block touches holds exactly 's intended final value, i.e., the post- state. Since this holds for every transaction independently, and transactions are applied to the journal (and hence recovered) in log order, the disk after recovery equals the state reachable by applying some prefix of committed transactions in order — precisely the "all-or-nothing, in order" guarantee required. The checksum on the commit record is what makes this robust to torn writes — a crash mid-write to the commit record itself must not be mistaken for a valid commit; without a checksum, a partially written commit record could be misinterpreted as valid, breaking invariant 1's guarantee at the boundary.
Ordered-mode data-safety argument. Ordered mode adds: data blocks for are written in place before 's commit record is durable. Combined with the commit-based atomicity above, this guarantees that whenever metadata claims a block belongs to file with certain content, that content is already correctly in place before the claim could ever become visible via recovery — ruling out the garbage-data exposure that writeback mode permits. It costs one additional ordering constraint (data-before-commit) but no extra journal writes for data, which is why it dominates in practice: near the safety of full data journaling, near the speed of writeback.
CoW correctness argument. The old tree, rooted at pointer , is never mutated — every write in an update targets freshly allocated blocks. The new tree, rooted at a freshly written block, is fully built and durable before the superblock write that installs . Because a single-sector (or single-block, protected by its own checksum) superblock write is atomic with respect to crashes on essentially all block devices, at any crash instant the visible root pointer is either (old tree, untouched, was already consistent) or (new tree, fully built before being pointed to). There is no third possibility and no replay step — the invariant "the current root always points to a fully-formed, self-consistent tree" is maintained by construction rather than repaired after the fact. This makes CoW recovery in the size of the interrupted update (just re-read the current superblock), though real systems still perform a small bounded amount of work to reclaim space from an aborted, never-linked-in tree.
Tradeoffs, stated plainly.
| Mechanism | Recovery time | Steady-state write cost | Data-safety guarantee |
|---|---|---|---|
| `fsck` | , disk-size dependent | none (no journal overhead) | metadata only, eventually |
| Writeback journaling | 1× data, 1× metadata-journal | metadata only, data may be stale | |
| Ordered journaling | 1× data, 1× metadata-journal | metadata + no-garbage data | |
| Full data journaling | 2× data, 1× metadata-journal | full atomic data+metadata | |
| Copy-on-write | – | write amplification from tree-path copies | full atomic data+metadata, snapshots free |
Worked example
Consider appending 4 KB of new data to an existing file `report.txt`, which requires three writes: (1) a new data block D, (2) an updated inode I (new size, new block pointer), and (3) an updated free-space bitmap B (mark D's block as allocated). Assume ext4-style ordered journaling with a 4-block journal region and this update assigned transaction id `T7`.
| Step | Action | Journal state | In-place state | If crash here… |
|---|---|---|---|---|
| 0 | Before update | empty | I=old, B=old, D=unwritten | trivially consistent (pre-update) |
| 1 | Write new data block D to its final in-place location (ordered-mode rule: data first) | empty | I=old, B=old, D=new | Recovery finds no `T7` records at all → discard nothing to discard; I/B still say "old size" so D's new bytes are simply unreferenced free space. Consistent. |
| 2 | Write `TxBegin(T7)` + metadata block images (new I, new B) into journal | `TxBegin, I', B'` | I=old, B=old, D=new | No commit record yet → `T7` discarded on recovery. I/B unchanged, still consistent; new D bytes remain unreferenced (leaked until next write reuses or a scrub reclaims, but not inconsistent). |
| 3 | Write `Commit(T7)` with checksum, force to disk | `TxBegin, I', B', Commit✓` | I=old, B=old, D=new | Recovery finds valid commit → replay: write I', B' to their in-place slots. Final state: I=new, B=new, D=new — fully consistent, update completes exactly as intended. |
| 4 | Checkpoint: write I', B' to in-place inode table and bitmap | `TxBegin, I', B', Commit✓` (still present, not yet reclaimed) | I=new, B=new, D=new | Recovery replays again (idempotent) — same final state. Consistent. |
| 5 | Mark `T7`'s journal space free for reuse | space reclaimed | I=new, B=new, D=new | Nothing to recover; steady state. |
Notice the key ordered-mode discipline at Step 1: data block D is written to its real location before the metadata transaction even begins in the journal — never the other way around. If the crash had happened between a metadata-first write and a later data write (which ordered mode forbids by construction), the inode could claim block D belongs to the file while D still held stale content from whatever previously occupied that block — the garbage-data hazard that ordered mode is specifically designed to prevent. Also notice that at Steps 1–2, a crash leaves unreferenced allocated-looking data (D was written, but no metadata points to it yet) — this is not a violation of consistency, merely a small, harmless leak that a future write can silently reclaim once the bitmap is regenerated or a background scrubber runs; it is never visible as corrupted file content, because no directory entry or inode ever pointed at it.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Assuming disk writes complete in program order | Disks/SSDs and OS caches may reorder or buffer writes for performance | Commit record can reach disk before its logged data, breaking the log-before-commit invariant |
| Skipping the commit-record checksum | Seems redundant if the record "looks" complete | A torn write during the commit record itself can be misread as a valid commit, replaying corrupt data |
| Confusing "journaled" with "safe from data loss" | Journaling only guarantees metadata/logged-data consistency, not that unflushed application buffers survive | Users still lose recently written data that was never `fsync`'d, even on a fully journaled FS |
| Using writeback mode and expecting no garbage data | Writeback journals metadata without ordering it after data | After crash, a file can validly point to blocks containing old, unrelated (possibly sensitive) data |
| Sizing the journal too small for the workload | Journal must hold all in-flight, uncheckpointed transactions | Journal fills up, forcing synchronous checkpoint stalls, hurting throughput under bursty writes |
| Treating CoW write amplification as free | Every update copies data plus every metadata block on the path to the root | Random small writes can multiply actual bytes written many times over, hurting flash endurance and latency |
| Forgetting idempotency requirements on replay | Replay may re-apply an already-checkpointed transaction | If replay logic isn't idempotent (e.g., it increments a counter instead of setting a value), double-replay corrupts state |
Pioneers & history
Origins · 1 minMendel Rosenblum and how this was discovered
Rosenblum, with Ozan Özkan and colleagues, and separately with John Ousterhout, pushed write-ahead logging to its logical extreme in the log-structured file system (LFS): instead of journaling updates to an otherwise in-place file system, treat the entire disk as one giant append-only log, turning every write — data and metadata alike — into a sequential append. This line of work is a direct ancestor of the copy-on-write file systems (ZFS, Btrfs, F2FS) that dominate flash and cloud storage today, since both share the core insight of never overwriting live structures in place.
Where this matters
Every mainstream production file system relies on one of these two families. ext3/ext4 (Linux's long-standing defaults) use ordered-mode write-ahead journaling via the JBD2 journaling layer, directly implementing the transaction lifecycle described above. NTFS uses a write-ahead log (`$LogFile`) modeled closely on database transaction logging, journaling metadata operations for Windows. XFS, built for large-scale servers, uses write-ahead logging with fine-grained, per-structure transactions to keep recovery fast even on very large volumes. ZFS and Btrfs use copy-on-write trees with checksummed blocks, enabling not just crash consistency but nearly free, consistent snapshots — because an old tree root, by construction, is a perfectly valid, frozen, consistent snapshot of the file system at that instant. F2FS, designed for flash storage, is explicitly log-structured to match flash's own append-friendly, erase-before-write characteristics. Database systems (PostgreSQL's WAL, MySQL's InnoDB redo log, SQLite's WAL mode) apply the identical write-ahead-logging principle one layer up the stack, to transactions over rows and pages rather than blocks and inodes — the same proof structure (log-before-commit, commit-before-checkpoint, idempotent replay) underlies both. Cloud block-storage and distributed file systems layer additional replication atop these single-machine techniques, which is exactly where the concerns of RAID and Redundant Storage pick up: journaling and CoW answer "how do we survive a crash on one device," while RAID and replication answer "how do we survive losing the device entirely."
Ask the tutor: How would ordered-mode journaling behave differently if the underlying disk silently ignores write-barrier (flush) commands?
Does journaling protect against losing recently written data that was never fsync'd?
No. Journaling guarantees that the file system's own metadata (and, in full-data mode, logged data) reaches a consistent, well-defined state after a crash — either a transaction fully applied or it didn't happen at all. It says nothing about application data still sitting in OS page-cache buffers that were never explicitly flushed via `fsync()` or an equivalent durability call. If an application writes a file and crashes (or the machine loses power) before that data is flushed to disk, journaling will correctly leave the file system structurally consistent, but the file's content may simply reflect its state as of the last actual flush — which could be seconds or more stale. Applications that need durability guarantees for their own data must explicitly request flushes; the journal protects the file system, not unflushed application buffers.
Why doesn't full data journaling simply replace ordered mode everywhere, since it's the safest option?
Full data journaling writes every data block twice — once into the journal, once to its final location — which for write-heavy workloads roughly halves effective disk bandwidth and adds latency, since journal writes are also typically sequential-then-committed, adding a round trip. Ordered mode gets nearly the same practical safety (no garbage data ever appears, and metadata is always transactionally consistent) at close to writeback speed, by exploiting a cheaper ordering constraint instead of a second full copy of the data. Full data journaling remains valuable for workloads where even transient exposure to overwritten-but-uncommitted data would be unacceptable (some database or security-sensitive deployments), but as a general-purpose default, ordered mode's cost/benefit ratio is better, which is why ext4 uses it by default.
How does copy-on-write achieve nearly free snapshots, and why can't ordinary journaling do the same?
In a CoW file system, an old root pointer, once superseded, still references a completely intact, self-consistent, and immutable tree of blocks — nothing was ever deleted or overwritten in place, only newly allocated. Simply keeping that old root pointer alive (instead of freeing its blocks) is a full, point-in-time, read-only snapshot, at the cost of only the blocks that later change (since unchanged blocks are shared between snapshot and live tree). Ordinary in-place file systems with journaling overwrite blocks directly, so an old state is destroyed the moment new data is written; recreating a snapshot would require copying the entire volume or maintaining a separate, expensive change-tracking mechanism, which is why journaled, in-place file systems generally rely on external snapshot tools (LVM, volume shadow copy) rather than getting snapshots for free from the consistency mechanism itself.
What exactly does "consistent" mean for a file system, precisely enough to check it?
A file system state is consistent if it satisfies its own structural invariants: every block marked allocated in the free-space bitmap is referenced by exactly one live inode (or is part of a valid metadata structure); every block referenced by a live inode is marked allocated; every directory entry names an inode that actually exists and is allocated; every inode's link count equals the number of directory entries that reference it; and no data block is referenced by more than one file unless the file system explicitly supports deduplication/shared extents. `fsck` and journaling both aim to guarantee this same target state after a crash — they differ only in how (by re-deriving it via a full scan, versus by never letting an inconsistent intermediate state become durable in the first place).
Can a journal itself become corrupted, and if so, what happens?
Yes — the journal is ordinary disk storage and subject to the same media errors, and a crash during a commit-record write is exactly the scenario the checksum is designed to catch. If the commit record's checksum does not match the logged blocks it claims to cover, recovery treats the entire transaction as uncommitted and discards it, falling back to the pre-transaction, already-consistent state; it never partially replays a transaction whose integrity it cannot verify. Some file systems additionally checksum the journal's transaction-begin records and use sequence numbers to detect a journal that has wrapped around and been partially overwritten, ensuring recovery never mistakes stale, reused journal space for a live, pending transaction.
Why is ordered journaling considered the practical "default" across so many production systems, and what does it give up compared to full data journaling?
Ordered journaling gives up strict data-write atomicity: if a crash occurs after data is written in place but before the corresponding metadata transaction commits, the data write itself is not guaranteed to be all-or-nothing at a sub-block granularity (though a single block write is atomic on virtually all real hardware, a multi-block data write for one large append is not). What it keeps is the property that matters most in practice — metadata is always consistent, and any block a live inode points to contains either its old content or its fully-written new content, never garbage from an unrelated deleted file. Combined with roughly half the I/O cost of full data journaling, this made ordered mode the default choice for ext3/ext4 and similarly influenced NTFS and XFS design, since the residual risk (a large append being partially applied at exactly the crash instant) is rare and usually tolerable for general-purpose workloads, unlike stale-security-sensitive-data exposure, which ordered mode fully eliminates.
How does this connect to what comes after a disk itself fails, not just a crash mid-write?
Journaling and copy-on-write both assume the underlying storage device is reliable once a write is acknowledged as durable — they solve the problem of partial, in-flight updates, not the problem of a disk that dies, develops bad sectors, or returns silently corrupted data. That is the domain of redundancy schemes such as RAID and Redundant Storage, which replicate or encode data across multiple physical devices so that the loss of an entire disk, not just an interrupted write, can be tolerated. In practice, production systems layer both: a journaling or CoW file system guarantees that any single acknowledged transaction is atomic and consistent, while RAID (or its distributed-systems analogues, like erasure-coded replication) guarantees that the storage backing those transactions survives hardware failure — the two mechanisms address orthogonal failure modes and are typically deployed together.