Recovery And Reliability
Checkpoints and Crash Recovery
The log grows forever. After a crash, how far
JrCodex·7 min read
Jr Codex DBMS Notes
Level: Advanced Prerequisites: Chapter 1: Failures and the Write-Ahead Log Time to complete: ~20 minutes
Table of Contents
- Why Checkpoints Exist
- Fuzzy Checkpoints
- The Three Recovery Phases
- Analysis
- Redo and Undo
- Crashing During Recovery
- Summary & Next Steps
1. Why Checkpoints Exist
The Problem Without Them
─────────────────────────────────────────
The log grows forever. After a crash, how far
back must recovery read?
Without checkpoints: to the BEGINNING OF TIME.
A database running for two years would take days
to restart.
─────────────────────────────────────────
What a Checkpoint Records
─────────────────────────────────────────
A CHECKPOINT record in the log stating:
- which transactions were ACTIVE at this moment
- which pages were DIRTY, and the LSN of the
earliest change not yet on disk
Recovery can then start from the checkpoint
rather than from the start of the log.
The checkpoint is a promise: everything before
this point is either on disk or described here.
─────────────────────────────────────────
2. Fuzzy Checkpoints
The Naive Version, and Its Flaw
─────────────────────────────────────────
A SHARP checkpoint:
1. stop accepting new transactions
2. wait for all active ones to finish
3. flush every dirty page
4. write the checkpoint record
5. resume
Correct, and the database is UNAVAILABLE for
however long step 3 takes — which on a large
buffer pool is seconds to minutes.
Unacceptable.
─────────────────────────────────────────
The Fuzzy Checkpoint
─────────────────────────────────────────
1. write a BEGIN_CHECKPOINT record
2. record the active transaction list and the
dirty page table
3. write an END_CHECKPOINT record
4. flush dirty pages GRADUALLY, in the
background, over the next several minutes
Transactions never stop. The checkpoint does not
claim the pages are on disk — it records exactly
WHICH ones are not, so recovery knows where to
start.
─────────────────────────────────────────
The Tuning Trade-off
─────────────────────────────────────────
FREQUENT checkpoints
──► short recovery, steady background I/O
RARE checkpoints
──► less I/O, LONG recovery, and large write
STORMS when one does happen
PostgreSQL: checkpoint_timeout,
max_wal_size, checkpoint_completion_target
(which spreads the flushing across the interval,
smoothing the I/O).
─────────────────────────────────────────
3. The Three Recovery Phases
The standard algorithm — ARIES — runs three passes over the log.
The Passes
─────────────────────────────────────────
1. ANALYSIS forward, from the last checkpoint
Determine WHO was running and WHICH
pages were dirty.
2. REDO forward, from the earliest dirty
page LSN
REPLAY EVERYTHING — including
transactions that later aborted.
3. UNDO BACKWARD, to the start of the
oldest loser
Roll back every transaction that
did not commit.
─────────────────────────────────────────
The Counter-Intuitive Part
─────────────────────────────────────────
Phase 2 redoes changes from transactions that
never committed — and phase 3 then undoes them.
Why not skip them?
BECAUSE recovery must first REPAIR THE STATE the
crash left, before it can reason about it. The
principle is "repeating history": reconstruct
the exact state at the moment of the crash, then
undo the losers from there.
Trying to be selective during redo would require
knowing which pages held which transactions'
changes — which is the information the crash
destroyed.
─────────────────────────────────────────
4. Analysis
def analysis(log, last_checkpoint):
"""Rebuild the state the crash destroyed: who was running, what was dirty."""
ck = log.read(last_checkpoint)
active = dict(ck.active_transactions) # txn -> last LSN seen
dirty = dict(ck.dirty_page_table) # page -> recovery LSN
for record in log.scan_forward(from_lsn=last_checkpoint):
if record.type == "BEGIN":
active[record.txn] = record.lsn
elif record.type in ("COMMIT", "ABORT"):
active.pop(record.txn, None) # finished ──► not a loser
elif record.type == "UPDATE":
active[record.txn] = record.lsn
dirty.setdefault(record.page_id, record.lsn) # first change since flush
losers = set(active) # still active at the crash
redo_start = min(dirty.values(), default=log.end_lsn)
return losers, dirty, redo_startThe Two Outputs
─────────────────────────────────────────
LOSERS transactions with no COMMIT or ABORT
record. They were in flight when the
machine stopped and must be undone.
REDO_START the earliest LSN whose change might
not be on disk. Everything before it
is definitely durable, so redo starts
here rather than at the checkpoint.
─────────────────────────────────────────
5. Redo and Undo
def redo(log, redo_start, buffer_pool):
"""Repeat history: reconstruct the exact state at the moment of the crash."""
for record in log.scan_forward(from_lsn=redo_start):
if record.type not in ("UPDATE", "CLR"):
continue
page = buffer_pool.get(record.page_id)
if page.lsn >= record.lsn:
continue # ← the IDEMPOTENCE check (Chapter 1, Sec.5)
# this change is already on the page
page.apply(record.after_image)
page.lsn = record.lsn
def undo(log, losers, buffer_pool):
"""Roll back the losers, newest change first, following prev_lsn chains."""
to_undo = {t: log.last_lsn_of(t) for t in losers}
while to_undo:
txn = max(to_undo, key=lambda t: to_undo[t]) # highest LSN first
record = log.read(to_undo[txn])
if record.type == "UPDATE":
page = buffer_pool.get(record.page_id)
page.apply(record.before_image) # restore the OLD value
clr = log.append(CLR(txn=txn, page=record.page_id,
undone_lsn=record.lsn,
next_undo=record.prev_lsn))
page.lsn = clr # ← log the undo itself
if record.prev_lsn:
to_undo[txn] = record.prev_lsn
else:
log.append(AbortRecord(txn))
del to_undo[txn]Compensation Log Records
─────────────────────────────────────────
The CLR is the clever piece.
When recovery undoes a change, it LOGS that undo
— with a pointer to what to undo NEXT.
So if the machine crashes again mid-undo, the
next recovery reads the CLRs, sees which undos
are already done, and continues from the right
place.
CLRs are REDO-ONLY: they are never themselves
undone. Undo therefore always makes forward
progress and can never loop.
─────────────────────────────────────────
6. Crashing During Recovery
The Requirement
─────────────────────────────────────────
Recovery may be interrupted at any point and
restarted from the beginning, any number of
times, and must still produce the correct state.
This is not a theoretical nicety — a machine that
crashed once is often about to crash again
(failing hardware, a bad power supply, an
out-of-memory loop).
─────────────────────────────────────────
What Makes It Safe
─────────────────────────────────────────
REDO IS IDEMPOTENT
The page_lsn check skips changes already
applied. Running redo five times equals running
it once.
UNDO IS RESUMABLE
CLRs record progress durably. A restarted undo
skips what is already undone.
ANALYSIS IS PURE
It only READS the log to compute two values. It
modifies nothing.
Each phase can therefore be re-run from scratch
with no ill effect.
─────────────────────────────────────────
The Idea Worth Carrying Away
─────────────────────────────────────────
"Log the intent before doing the work, and make
replay idempotent."
It is the same pattern as:
- a filesystem journal
- an idempotency key on a payment API
(Module 3, Chapter 6's UPSERT)
- completed_tool_calls in a durable agent
(Agentic AI Notes, Module 6, Chapter 2)
Learn it once here, where it is at its most
rigorous, and you will recognise it everywhere.
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- Checkpoints bound recovery time by recording which transactions were active and which pages were dirty, so recovery need not read the whole log.
- Fuzzy checkpoints record that state without stopping transactions or waiting for flushes, trading a slightly longer recovery for continuous availability.
- Recovery redoes everything including aborted transactions — repeating history first, then undoing losers — because being selective would need information the crash destroyed.
- Compensation log records make undo resumable, and the page-LSN check makes redo idempotent, so recovery survives crashing during recovery.
Concept Check
- Why does a sharp checkpoint make the database unavailable, and how does a fuzzy checkpoint avoid it?
- Why does the redo phase replay changes from transactions that are about to be undone?
- What role do CLRs play, and why are they never themselves undone?
Next Chapter
→ Chapter 3: Backup, Replication and Availability
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index