Home › Databases & storage › Durability & recovery
algorithm low

Write-ahead log

Problem Databases, queues and metadata services must not lose or corrupt committed data when the process crashes or power fails halfway through updating files.

Append each change to a checksummed sequential log and fsync it before touching data pages; the basis of crash durability in nearly every database.

append O(1); commit = 1 fsync, shared through group commit; recovery O(log since last checkpoint)
durabilitycrash-recoveryfsynclogging
Overview

Updating data pages in place can be torn by a crash halfway. So first append a record that describes the change to a sequential log and make it durable. The pages can then be written whenever it suits you. After a crash, replay the log. Commit latency is one sequential fsync instead of many random page writes. Databases, queues, coordination services and journaling file systems all rely on it.

Key facts

Use when

  • Any store that must survive a crash or power loss: databases, queues, metadata services.
  • Journaling file systems, message logs and coordination services (see In production), not only databases.

Avoid when

  • The data can be rebuilt (caches, derived indexes).
  • Copy-on-write structures are durable by construction (LMDB-style shadow paging); then you need only an atomic root swap.

Deep dive

How it works

In: change records (and commit markers) from writers · Out: a durable, ordered log; after a crash, the replayed state · State: log segments (PostgreSQL: 16 MiB), a flushed-LSN, the last checkpoint's redo point · Cost: append O(1); commit = 1 fsync (shared via group commit); recovery O(log since the last checkpoint).

  1. WAL rule: a page whose pageLSN = x may go to disk only after the log is durable through x. The buffer pool checks this before every write-back.
  2. Commit rule: a commit returns only after its commit record is durable.
  3. Framing: [len u32][crc32c u32][lsn u64][payload]. The CRC covers the LSN and the payload.
  4. Recovery: start at the last checkpoint, read records, verify each CRC and redo. The first short or bad record marks the torn tail. Truncate there and continue from that point.
  5. Checkpoint: flush dirty pages (or log a fuzzy checkpoint with the dirty page table, see ARIES), record the redo start point, recycle older segments.
segment 000001          segment 000002 (preallocated)
[rec][rec][rec]...      [rec][rec][ torn | zeros ...]
               ^ checkpoint redo point      ^ recovery stops here and truncates

Code

use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};

fn crc32c(data: &[u8]) -> u32 { // bitwise; real code uses the SSE4.2 / ARMv8 CRC32C instruction
    let mut crc = !0u32;
    for &b in data {
        crc ^= b as u32;
        for _ in 0..8 { crc = (crc >> 1) ^ (0x82F6_3B78 & (crc & 1).wrapping_neg()); }
    }
    !crc
}

pub struct Wal { f: File, buf: Vec<u8>, next_lsn: u64 }

impl Wal {
    /// Opens the log, returns every intact record, and truncates a torn tail.
    pub fn open(path: &str) -> io::Result<(Wal, Vec<(u64, Vec<u8>)>)> {
        let mut f = OpenOptions::new().read(true).append(true).create(true).open(path)?;
        let mut data = Vec::new();
        f.read_to_end(&mut data)?;
        let (mut pos, mut recs) = (0usize, Vec::new());
        while pos + 16 <= data.len() {
            let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
            let crc = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
            let end = pos + 16 + len;
            if end > data.len() || crc32c(&data[pos + 8..end]) != crc { break; } // torn tail
            let lsn = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
            recs.push((lsn, data[pos + 16..end].to_vec()));
            pos = end;
        }
        f.set_len(pos as u64)?; // new records must follow the last valid one
        f.sync_all()?;
        let next_lsn = recs.last().map_or(1, |r| r.0 + 1);
        Ok((Wal { f, buf: Vec::new(), next_lsn }, recs))
    }

    /// Buffers a record; not durable until `flush` returns.
    pub fn append(&mut self, payload: &[u8]) -> u64 {
        let lsn = self.next_lsn;
        self.next_lsn += 1;
        let start = self.buf.len();
        self.buf.extend_from_slice(&(payload.len() as u32).to_le_bytes());
        self.buf.extend_from_slice(&[0; 4]); // CRC placeholder
        self.buf.extend_from_slice(&lsn.to_le_bytes());
        self.buf.extend_from_slice(payload);
        let crc = crc32c(&self.buf[start + 8..]);
        self.buf[start + 4..start + 8].copy_from_slice(&crc.to_le_bytes());
        lsn
    }

    /// Durable through every LSN appended so far: one write + one fdatasync.
    pub fn flush(&mut self) -> io::Result<()> {
        self.f.write_all(&self.buf)?;
        self.buf.clear();
        self.f.sync_data() // fdatasync: data and file size, skips mtime
    }
}

This uses LSN = record number. PostgreSQL and InnoDB use the byte offset in the log as the LSN, so an LSN also locates its record.

Complexity and constants

  • Sequential append bandwidth is order of GB/s on NVMe. Commit rate without batching is about 1 / fsync latency: 1 ms gives 1000 commits/s per log.
  • Record overhead: 16-24 B header. PostgreSQL record headers are 24 B, plus a full 8 KiB page image the first time each page changes after a checkpoint, which often dominates WAL volume.
  • Recovery time ≈ log since checkpoint / redo speed. Redo is often bound by random page reads, so it is order of minutes for GBs of log on slow disks. PostgreSQL defaults: checkpoint_timeout = 5min, max_wal_size = 1GB.

Efficiency

  • Cost: sequential append is order of GB/s on NVMe; fsync is order of 10-50 µs on power-loss-protected enterprise NVMe, 0.1-several ms on consumer SSDs, 5-10 ms on HDD. Without batching, commits/s ≈ 1 / fsync latency (1 ms gives 1000/s per log). Records carry a 16-24 B header, plus a full page image per page after each checkpoint (PostgreSQL). Bound: fsync latency, not bandwidth.
  • Levers:
    • group commit — one fsync for all waiting commits; PostgreSQL commit_delay helps only if other commits arrive in the delay — concurrent writers
    • synchronous_commit = off — can lose recent commits, stays consistent — loss-tolerant writes
    • wal_compression (pglz, lz4, zstd) — compresses full-page images for some CPU — image-dominated WAL volume
    • longer checkpoint intervals — fewer full-page images, longer recovery (PostgreSQL defaults: checkpoint_timeout = 5min, max_wal_size = 1GB) — recovery time budget allows it
    • redo-only no-steal logging, command logging, or copy-on-write pages — smaller or no log — in-memory databases, single-writer stores
  • Naive → standard → state of the art:
    • write pages in place at commit · many random page writes, torn pages
    • WAL with CRC and one fsync per commit · 1 / fsync latency commits/s
    • group commit on power-loss-protected NVMe · fsync 10-50 µs shared by a batch

Implementation notes

  • Preallocate segments (fallocate and zero-fill, or recycle old files). Appending to a preallocated file means fdatasync does not have to update metadata. Growing a file costs an extra journal commit in the filesystem.
  • fsync the directory after creating or renaming a segment, or the file itself can vanish after a crash.
  • O_DIRECT + O_DSYNC with 4 KiB-aligned writes skips the page cache (InnoDB innodb_flush_method=O_DIRECT). Pad the last partial block, and do not overwrite a block that holds acknowledged records unless the format tolerates a torn rewrite.
  • macOS: fsync does not flush the drive's cache. You need fcntl(F_FULLFSYNC), which costs order of ms.
  • Checksums: CRC32C runs at more than 10 GB/s with the hardware instruction. xxHash is fine when hardware CRC is missing.
  • Record kinds: physical (byte ranges or page images), logical (operations), or physiological (per-page logical, the ARIES default). Physical redo is simpler; logical records are smaller.

Pitfalls

  • fsync errors: on Linux, after a failed fsync the dirty pages may be dropped and the error cleared, so a retried fsync "succeeds" and data is lost. Treat any fsync failure as fatal, then crash and recover from the log (PostgreSQL has done this since 2018).
  • Telling a torn tail from mid-log corruption: a bad record followed by valid ones is corruption, not a crash. Stop and alert instead of truncating.
  • Drives that ignore flush commands (cheap SSDs, some VMs) make every guarantee void. Test with power-pull tools, and prefer drives with power-loss protection.
  • The log grows without bound if checkpoints stall or a replica holds a replication slot.
  • Writing the data page before its log record breaks the WAL rule and leaves no way to undo or redo.

Variants and alternatives

Redo-only (no-steal)dirty pages of uncommitted transactions never reach disk, so no undo is needed. Common in in-memory databases.
Undo/redo with ARIESsteal and no-force buffer pools.
Command logginglog the transaction inputs and replay them deterministically (VoltDB). Tiny logs, slower recovery.
Shadow paging / copy-on-writeLMDB, SQLite rollback journal (undo journal of page images).
The log is the databaseKafka, Raft logs, event sourcing; see event sourcing.

In production

  • Databases: PostgreSQL WAL, InnoDB redo log, SQLite WAL mode, RocksDB and LevelDB WAL.
  • Message logs and coordination: Kafka segments, etcd WAL, ZooKeeper transaction log.
  • File systems: journaling file systems such as ext4 (jbd2) and NTFS keep a log of metadata changes.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
raft-engineRusttikv/raft-enginecargo add raft-engine————log-structured engine with one shared log stream; TiKV's Raft log store
fjallRustfjall-rs/fjallcargo add fjall————LSM store with a journal; PersistMode picks the flush level
RocksDBC++facebook/rocksdbbrew install rocksdb————WAL in front of the memtable; Rust binding: cargo add rocksdb
SQLiteCsqlite/sqlitebrew install sqlite————WAL mode via PRAGMA journal_mode=WAL
tidwall/walGotidwall/walgo get github.com/tidwall/wal————small standalone write-ahead log library

Sources

  • C. Mohan et al., ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging, ACM TODS, 1992. paper
  • J. Gray, A. Reuter, Transaction Processing: Concepts and Techniques, 1992.
  • T. S. Pillai et al., All File Systems Are Not Created Equal: On the Complexity of Crafting Crash-Consistent Applications, OSDI, 2014. paper
  • A. Rebello et al., Can Applications Recover from fsync Failures?, USENIX ATC, 2020. paper
  • PostgreSQL documentation, Write Ahead Log settings (synchronous_commit, commit_delay, wal_compression, full_page_writes). docs
  • SQLite documentation, Write-Ahead Logging. docs

Builds on

Used by

ARIES recoveryA database with a disk buffer pool that writes uncommitted changes out early and commits without flushing pages must restore a consistent state after a crash, keeping committed work and removing unfinished work.
batch checkpoint restart · plannedLarge batch jobs (end-of-day, settlement, payroll runs) that checkpoint and restart idempotently
bft consensus · plannedByzantine fault tolerant consensus (PBFT, Tendermint, HotStuff) with 3f+1 replicas and quorum certificates
Chain replicationA storage system keeping several copies of data in one datacenter must stay consistent and survive node crashes while moving large amounts of data. This is a way to replicate writes without voting.Change data captureTeams must keep a search index, cache, warehouse or other service in step with a live database, deletes included, without changing every writer, polling the tables or losing changes in a crash.
crash consistency ordering · plannedOrdered writes, barriers and FUA so on-disk state is valid after power loss at any point
Durable job queueBackground work such as emails, webhooks, media processing and billing must not vanish if a worker or the queue crashes. This makes sure every submitted job eventually runs, with retries.FIX protocol (session layer)Two firms exchanging orders over a network connection that can drop must continue after a disconnect without losing or duplicating any order or reply.Group commitA database log, queue or consensus system with many concurrent writers that each need data durably on disk before replying is limited by the cost of each disk flush.Hinted handoffA replicated store must keep accepting writes while one of its copies is unreachable for minutes or hours, without losing the updates that copy missed, and let it catch up cheaply when it returns.Incremental sync cursorsMobile and desktop clients that mirror a server collection (mail, calendar, files, settings) waste bandwidth and battery by re-downloading everything. This syncs only what changed since the last visit and resumes after a disconnect.Matching engineAn exchange must decide which buy and sell orders trade, in a fair and reproducible order, and after a crash end up with exactly the same result.Multi-PaxosReplicated systems need several machines to agree on one ordered log of commands despite failures. This is the classic consensus protocol, preferred when you need more flexibility than Raft offers.Point-in-time recoveryAfter a bad deploy, an exploit or an accidental delete, an operator must bring a database or service back to its exact state of a moment before the damage, without restoring only to the last nightly copy.Primary-backup replicationA stateful service on one machine must keep running after that machine dies, without losing updates it already confirmed to clients, at the price of extra machines and some added write latency.RaftSeveral machines must agree on an ordered log of commands and keep identical state, surviving failures, for metadata, locks, configuration or sharded databases. This is the understandable consensus protocol for that.State-machine snapshotsA service rebuilds its state by replaying an ever-growing log, so restart and new-replica catch-up get slower and the disk fills. It needs a bounded log and a fast restart without losing or duplicating a single update.
total order sequencer · plannedSingle sequencer stamps every inbound message so replicated deterministic state machines (LMAX style) replay identically
Two-phase commitA transaction touching several shards or databases must either take effect everywhere or nowhere, even if a participant or the coordinator crashes mid-way. This gives that all-or-nothing outcome.
viewstamped replication · plannedVSR: primary-backup consensus with view changes and recovery

Systems (66):

Alternatives and related