Log-Based Crash Recovery
Imagine a bank transfer that debits one account, then the power fails before crediting the other. Money vanishes. Every database system must guarantee that this cannot happen. A crash must leave the database looking as if either the whole transaction happened, or none of it did. This is the atomicity and durability half of ACID Properties. It must hold even when the crash occurs at the worst possible instant. This includes mid-write, with the buffer cache half-flushed to disk. Log-based crash recovery is the mechanism that makes this guarantee practical. Forcing every transaction's changes to disk before it commits would be prohibitively slow. Instead, the system records a compact history of what changed in a sequential log. The actual database pages are written back to disk whenever it is efficient. After a crash, the system replays this log to reconstruct a consistent state. It redoes work that was committed but not yet reflected on disk. It also undoes work that was in progress but never committed. This idea matters because it decouples performance from safety. Sequential log writes are cheap (append-only, one disk seek amortized over many records). Random writes of data pages, by contrast, are expensive. By logging first and writing data pages later, systems achieve both high throughput and provable recoverability. Virtually every production database is built on some variant of this technique. This includes PostgreSQL, MySQL/InnoDB, Oracle, SQL Server, and SQLite's WAL mode. This article develops the classical model of log-based recovery. It covers what a log record contains, and how the log enables undo and redo. It also covers the two dominant strategies that follow from this model. These are deferred (redo-only) recovery and immediate (undo/redo) recovery. Each follows from different Transaction States and Atomic Execution policies for buffer management. The article sets up, without yet detailing, the ideas of Write-Ahead Logging, checkpointing, and the ARIES algorithm. ARIES generalizes and hardens this scheme in real systems.
- A recovery log is an append-only sequence of records describing every update, written before the corresponding data page reaches stable storage.
- Log records carry enough information — old value, new value, transaction id — to redo an update (reapply it) or undo it (reverse it).
- Deferred-update systems only ever need REDO; immediate-update systems need both UNDO and REDO because uncommitted changes may already be on disk.
- The Write-Ahead Logging (WAL) rule — log record hits stable storage before the data page it describes — is the invariant that makes recovery possible at all.
- Recovery scans the log, classifies transactions as committed or unfinished, undoes the unfinished ones, and redoes the committed ones (order and precision depend on the scheme).
- Checkpoints bound how far back the log scan must go, trading a little runtime overhead for vastly faster recovery.
If every change is durably logged before it touches disk, a crash can always be undone or redone from the log — this single ordering rule is what makes atomic, durable transactions possible on unreliable hardware.
The idea
A recovery log (or transaction log) is a sequential, append-only file of log records, each describing one state-changing event: a transaction starting, a transaction updating a data item, a transaction committing, or a transaction aborting. The log lives on stable storage — storage assumed to survive crashes and, ideally, media failure (mirrored disks, replicated storage). The database's actual data pages live in a buffer pool in volatile memory and are written back ("flushed") to disk pages at times the buffer manager chooses, not necessarily when a transaction commits. An update log record for transaction writing data item has the form
recording the transaction id, the item modified, its value before the write (, needed for undo), and its value after the write (, needed for redo). Special records mark control events:
- — has begun.
- — 's updates are permanent; this is the point of no return.
- — has been rolled back.
The central invariant governing all of this is the Write-Ahead Logging (WAL) rule, previewed here and detailed in Write-Ahead Logging: > Before a data page containing an update by is written to disk, the log record for that update must already be on stable storage. Before a transaction commits, all its log records (including the commit record) must be on stable storage. Given WAL, the log always contains a strictly complete or ahead picture of what has happened to the database, relative to what is actually on disk. That asymmetry — log always at least as current as data — is exactly what recovery exploits: any update visible on disk is guaranteed to also be recorded in the log, so the log alone suffices to reconstruct or repair the database state after a crash.
How it works
Recovery must handle two categories of transactions active at crash time, discovered by scanning the log: 1. Committed transactions whose record is in the log — their effects must survive (be redone if not already reflected on disk). 2. Uncommitted / unfinished transactions with a but no or — their effects must be erased (undone), because atomicity forbids partial results. Two classical policies govern how buffer pages relate to logging, and each yields a different recovery algorithm.
Deferred database modification (redo-only recovery). A transaction's updates are only written into its log records while it runs; the actual database pages are not modified until after the transaction commits. During normal execution, uncommitted changes exist only in the log and in private buffers, never on the shared disk copy. Consequently, at crash time no uncommitted transaction could possibly have modified the on-disk database — so no undo is ever needed. Recovery only has to redo committed transactions, in the order their updates were logged, to make sure their effects are actually reflected on disk. This scheme is simple and elegant but has practical costs: buffering all writes of a possibly long transaction requires extra memory, and it precludes early flushing that concurrency and buffer-pressure sometimes need.
Immediate database modification (undo/redo recovery). Data pages may be updated on disk while the transaction is still running, as soon as WAL has logged the corresponding old/new values. This is what real buffer managers do — they cannot wait for commit before evicting a dirty page. Now an uncommitted transaction's changes might already be on disk when the crash hits, so recovery needs both operations:
- UNDO: for every unfinished transaction, restore each item it touched to , scanning the log backward (most recent update first) so that multiple updates to the same item are undone in the correct reverse order.
- REDO: for every committed transaction, reapply to each item it touched, scanning the log forward, so that if a page was lost or an update never made it to disk, it is correctly reinstated.
Both scans are needed because a page on disk might reflect a mixture — some committed, some uncommitted work interleaved by the buffer manager — and only the log's global order tells you the truth about each individual write.
The recovery algorithm
The standard two-pass procedure (analysis conceptually folded into the passes here; the ARIES scheme formalizes an explicit analysis pass, foreshadowed but not detailed) works as follows: ``` procedure RECOVER(): undo_list := {} # transactions needing rollback redo_list := {} # transactions to reapply
# ---- Pass 1: scan the log forward, classify transactions ---- for each record r in log, in the order written: if r = : add Ti to undo_list else if r = or r = : remove Ti from undo_list if r = : add Ti to redo_list
# after this pass:
# undo_list = transactions with start but no commit/abort -> losers
# redo_list = transactions with a commit record -> winners
# ---- Pass 2: undo losers, scanning backward ---- for each record r in log, in REVERSE order: if r = and Ti in undo_list: write V_old to data item X on disk append to log # compensation record (optional variant) for each Ti in undo_list: append to log
# ---- Pass 3: redo winners, scanning forward ---- for each record r in log, in the order written: if r = and Ti in redo_list: write V_new to data item X on disk flush all modified pages to stable storage ``` For deferred modification, the algorithm collapses: `undo_list` is provably always empty (no uncommitted transaction ever wrote to disk), so Pass 2 is skipped entirely and only Pass 3 (REDO of committed transactions) executes. A subtlety in Pass 2 deserves attention: undoing must proceed backward through the log. If wrote twice — first , then — undoing forward would restore to then immediately overwrite it back to from the first record misapplied out of order; undoing backward correctly restores then , landing on the true pre-transaction value. Real systems (as in ARIES) also write compensation log records (CLRs) during undo, so that if the system crashes again during recovery, the partial undo already performed is never repeated — a detail this article foreshadows for the fuller treatment of ARIES Recovery.
Complexity & correctness
Time complexity. Let be the number of log records written since the last checkpoint (or since the log began, absent checkpointing). Each pass over the log performs work per record — a comparison of transaction id against membership in a hash set, plus one disk write per data-item update touched during undo/redo. So a full recovery costs
I/O and CPU work, dominated in practice by the random disk writes needed to reinstate or roll back individual pages, not by the sequential log scan itself (which is cheap, since the log is read sequentially — good locality). Because can in principle grow without bound between checkpoints, unbounded log growth means unbounded worst-case recovery time; this is precisely the problem checkpoints solve (foreshadowed, detailed under Checkpoints): a checkpoint record fixes a known-consistent point, and recovery can restrict its backward scan to transactions active at or after that point, plus a forward redo starting there — bounding to "log since last checkpoint" rather than "log since time zero."
Space complexity. The log itself is records, each size for a fixed-size item (larger for variable-length values, but still bounded by the size of the update). Recovery needs auxiliary memory, where is the number of transactions active at crash time, to hold the undo/redo transaction-id sets — negligible compared to database size.
Correctness argument. The key claim to justify is: after RECOVER() terminates, the database is exactly as if every committed transaction ran to completion and every uncommitted transaction never ran at all (the Durability and Atomicity guarantees of ACID Properties). The argument rests on three facts: 1. WAL guarantees the log is never behind reality. Because a data page write is preceded by its log record hitting stable storage, at crash time, for any update reflected on disk, the corresponding log record is guaranteed present. The converse can fail (a log record may exist for an update not yet on disk) — but that asymmetry is exactly what redo is for. 2. Classification is exhaustive and exclusive. Pass 1 partitions every transaction that ever wrote a `start` record into exactly one of: committed (has a `commit` record — put in redo_list) or unfinished (no `commit`/`abort` — put in undo_list). No transaction is double-counted because commit/abort removes it from undo_list, and no transaction is missed because every update log record belongs to some transaction with a `start` record earlier in the log (transactions cannot write without starting). 3. Idempotence of undo/redo makes reapplication safe regardless of what was already on disk. Applying to (redo) or to (undo) results in the same final value of whether or not that write had already happened before the crash. Formally, redo and undo actions are idempotent: applying them once or many times yields the same state. This matters because recovery cannot know, for a given item, exactly how far the previous flush got — it simply reapplies every relevant log record and trusts idempotence to make the outcome correct regardless. This is also why recovery is safe to re-run if it is itself interrupted by a second crash: repeating Pass 2/3 changes nothing once the correct final values are already in place. Given (1)–(3): every committed transaction's final writes are guaranteed to end up on disk (by redo, forward order preserves multiple-write ordering correctly since later log records for the same item overwrite earlier reapplications with the true final value), and every unfinished transaction's writes are guaranteed erased (by undo, backward order correctly unwinds multiple writes to the same item). No partial transaction outcome can survive.
Honest tradeoffs. Deferred-update recovery is simpler (no undo logic at all) but forces transactions to buffer all their writes until commit, which is memory-expensive and blocks early write-back under memory pressure — a poor fit for long-running or bulk-update transactions. Immediate-update recovery scales better in normal operation but requires a correct, carefully ordered undo pass and, in production systems, additional machinery (compensation log records, dirty-page tables) to make recovery itself crash-safe — machinery that ARIES Recovery formalizes. Both schemes' recovery time grows with unbounded log length absent checkpointing, which is why virtually no production system omits checkpoints.
Worked example
Consider three transactions on an account database with items , , (initial values ), using immediate modification. The log accumulated before a crash:
| # | Log record | Meaning |
|---|---|---|
| 1 | `` | begins |
| 2 | `` | debits by 100 |
| 3 | `` | credits by 100 |
| 4 | `` | finishes |
| 5 | `` | begins |
| 6 | `` | debits by 50 |
| 7 | `` | begins |
| 8 | `` | credits by 50 |
| 9 | `` | writes (e.g. a large withdrawal) |
| — | CRASH | power fails here |
Note: `` never appears — was interrupted mid-transaction. never committed either. Because this is immediate modification, the buffer manager may have already flushed some of these writes to disk (say pages holding and were flushed, 's page was not) — recovery must work correctly regardless of which subset actually reached disk, which is the point of idempotent redo/undo.
Pass 1 — classify.
- : has `start` (record 1) and `commit` (record 4) → redo_list.
- : has `start` (record 5), no commit/abort → undo_list.
- : has `start` (record 7), no commit/abort → undo_list.
Pass 2 — undo, scanning backward from record 9 to record 1, acting only on records belonging to :
| Scanned record | Action |
|---|---|
| 9: `` | undo_list → write (restore old value) |
| 8: `` | undo_list → write |
| 6: `` | undo_list → write |
| (records 1,4,5,7 not update records — skipped) |
After Pass 2: append `` and `` to the log. At this point — exactly 's post-commit state, with and fully erased.
Pass 3 — redo, scanning forward, acting only on records belonging to :
| Scanned record | Action |
|---|---|
| 2: `` | redo_list → write |
| 3: `` | redo_list → write |
| (records 6,8,9 belong to non-redo transactions — skipped) |
Final state: . Check: this is exactly the state after alone runs to completion from the initial values (, ), with 's and 's partial work fully absent, correctly restored to its pre- value of . Every dollar is accounted for — atomicity held even though pages were flushed in an arbitrary, interleaved order before the crash.
Common pitfalls & traps
| Pitfall | Why it breaks recovery |
|---|---|
| Undoing in forward log order instead of backward | Multiple writes to the same item get unwound out of order, leaving a wrong intermediate value instead of the true pre-transaction value |
| Flushing a data page before its log record is stable (violating WAL) | The log can no longer prove what happened; a crash right after can lose the update with no way to redo it |
| Forgetting that redo/undo must be idempotent | Without idempotence, re-running recovery after a second crash mid-recovery could double-apply an update and corrupt data |
| Treating "no commit record" and "abort record present" as different cases needing different handling | Both mean the same thing for recovery — undo — collapsing this distinction is fine and simplifies the algorithm, but omitting either case from undo_list is a bug |
| Assuming deferred-update recovery never needs to worry about buffer eviction | Deferred systems still need enough buffer space to hold all of a transaction's dirty pages until commit; running out of memory mid-transaction is a real operational failure mode, not merely a "textbook simplification" |
| Ignoring checkpoints entirely | Recovery must scan the entire log from the beginning of time, making recovery time unbounded and start-up after a crash arbitrarily slow |
Pioneers & history
The theory and engineering of log-based recovery grew out of the transaction-processing research of the 1970s–80s, much of it at IBM, as relational databases moved from research prototypes (System R) to industrial systems requiring real fault tolerance.
Origins · 1 minJim Gray and how this was discovered
Jim Gray was the principal architect of the transaction concept as an engineering discipline, not merely a theoretical abstraction. His work on System R at IBM and later at Tandem formalized the notions of logging, locking, and recovery that underlie every commercial database, and his 1981 Turing Award-adjacent survey "The Transaction Concept: Virtues and Limitations" remains foundational reading. He received the ACM Turing Award in 1998 for these contributions to database and transaction processing systems.
Where this matters
Log-based recovery is not a textbook abstraction — it is the load-bearing mechanism inside nearly every production database and storage engine. PostgreSQL's WAL subsystem, MySQL/InnoDB's redo log and undo log tablespaces, Oracle's redo log and rollback segments, SQL Server's transaction log, and SQLite's WAL mode all implement variants of the redo/undo scheme described here, layered with checkpointing and, in most, an ARIES-style analysis/redo/undo restart procedure. Distributed systems built on top — Kafka's log-structured storage, etcd and Raft-based consensus logs, cloud data warehouses' commit logs — borrow the same append-first-then-apply discipline for the same reason: sequential writes are fast and durable, and a log of intent is easier to reason about than mutable shared state. Anywhere a system must survive a crash without losing or duplicating work — banking, e-commerce order processing, distributed file systems, message queues — some descendant of this algorithm is running underneath.
Ask the tutor: How do compensation log records make undo itself crash-safe during a second failure?
What exactly goes into a log record, and why does it need both the old and new value?
A standard update log record is ``: transaction id, the item written, the value before, and the value after. The old value is needed for undo — if never commits, its effect must be erased by restoring to . The new value is needed for redo — if commits but its write never reached disk before a crash, recovery must reapply . Storing both lets one log record serve both possible recovery needs without knowing in advance whether the transaction will commit or abort, and without knowing whether the buffer manager already flushed the page.
Is the recovery log the same thing as a database backup?
No. A backup is a full (or incremental) copy of the database state at some point in time, used to recover from media failure — a destroyed disk, corrupted files. The recovery log is a record of changes, used to recover from a system crash that loses volatile memory but leaves the disk intact (aside from possibly-incomplete writes). In practice the two combine: media recovery restores the last backup and then replays the log from that point forward, giving durability even against disk loss, a topic that extends beyond crash recovery into media recovery and archival logging.
Why can't the database just flush every write to disk immediately and skip logging altogether?
It could, in principle, but at ruinous cost. Data pages are scattered randomly across disk, so flushing on every write means a random I/O per update — orders of magnitude slower than sequential log appends. Logging lets the system batch and delay data-page writes (the buffer manager decides when, based on memory pressure and efficiency) while still guaranteeing recoverability, because the log — not the data pages — is the source of truth about what happened. This separation of "record intent fast" from "apply lazily" is the core efficiency argument for the entire technique.
What happens if the system crashes again while recovery itself is running?
This is exactly why redo and undo are designed to be idempotent, and why production systems (ARIES in particular) write compensation log records during undo. Idempotence guarantees that reapplying an already-applied redo, or re-undoing an already-undone update, leaves the state unchanged — so simply restarting the whole recovery procedure from scratch is safe. Compensation log records go further, recording exactly how far an undo got, so a second recovery does not have to redo undo work already completed, which matters more for efficiency than correctness in the basic scheme.
How does log-based recovery relate to checkpoints and to ARIES?
This article's redo/undo scheme is the conceptual foundation; two refinements make it practical. Checkpoints periodically record a known-consistent snapshot of which transactions are active, letting recovery skip scanning the log all the way back to the beginning — bounding recovery time. ARIES generalizes the whole scheme into three explicit passes (analysis, redo, undo), adds a dirty-page table for precise redo, and uses compensation log records for crash-safe undo. Both topics are natural next steps once the basic redo/undo logic here is understood.
Does log-based recovery handle concurrent transactions correctly, or does it assume transactions run one at a time?
The scheme as described handles concurrency correctly precisely because log records are interleaved by transaction id and timestamp order, not grouped by transaction. Multiple transactions' updates appear interleaved in one global log, and the classification pass (commit vs. no-commit) and the backward/forward replay passes work per-item and per-transaction regardless of how interleaved the original execution was. Correctness additionally depends on the concurrency control protocol (locking or otherwise) having produced a serializable or otherwise well-formed execution in the first place — recovery does not fix a badly-interleaved schedule, it only restores consistency for whatever valid schedule occurred.
Why is the commit record specifically singled out as "the point of no return"?
Once `` is durably on the log, the transaction is defined to have succeeded permanently — this is the durability guarantee. Recovery treats the presence of this record as the sole criterion for redoing a transaction's effects, no matter what else happens afterward. This is why WAL requires the commit record itself to be flushed to stable storage before the commit is reported successful to the user or application: if the system crashes after telling a user "transaction committed" but before the commit record reached stable storage, recovery would incorrectly treat that transaction as a loser and undo it, breaking the durability contract.