Back to Database Internals: B-tree, MVCC, WAL
Q1Question 1 of 1

Write-Ahead Logging (WAL) and how it shapes a ledger backup strategy

Key Concepts
  • WAL writes every change to a sequential log and fsyncs it to disk BEFORE modifying the actual data pages, decoupling durability from random page writes.
  • On crash, PostgreSQL replays the WAL from the last checkpoint forward, redoing committed changes and discarding uncommitted ones.
  • Streaming replication ships WAL records continuously to replicas, which apply them to stay in sync with the primary.
  • Point-in-time recovery (PITR) restores a base backup and replays archived WAL segments up to any chosen timestamp.
  • A ledger backup strategy uses periodic base backups plus continuous WAL archiving to durable storage like S3, giving essentially zero committed-data loss.
  • Checkpoints bound how much WAL must be replayed on recovery; more frequent checkpoints mean faster recovery but more write amplification.
  • WAL archiving must itself be durable and monitored, because a gap in archived segments breaks the point-in-time recovery chain.

Definition

Log the change durably first, update the data pages after: so a crash is recovered by replaying the log, and archived logs plus a base backup let you restore to any moment.
  • Write-Ahead Logging (WAL) – A durability protocol where every change is first appended to a sequential log and flushed to disk (fsync) BEFORE the corresponding data pages are modified.
  • Crash recovery – On restart after a crash, the database replays the WAL from the last checkpoint, redoing committed transactions and discarding uncommitted ones.
  • Replication – A replica is another server replaying the same WAL stream, staying in sync via streaming replication.
  • Point-in-time recovery – A base backup plus archived WAL segments lets you restore to any chosen timestamp, not just last night's snapshot.

At the National Bank of India, before a clerk changes any balance in the big ledger, they first write the exact instruction: 'debit SAV2001 by 50,000' into a sequential diary and make sure the ink is dry (fsync). Only then do they update the ledger page. If the power fails mid-update, the bank reopens the diary and re-applies every instruction that was written down, so no committed transaction is ever lost and the ledger is reconstructed exactly.

Understanding the Concepts

What is Write-Ahead Logging?

Write-Ahead Logging is a durability protocol at the heart of databases like PostgreSQL. The principle is in the name: before any data page is modified, the database first appends a compact record of the intended change to a sequential log and fsyncs that log to disk. Only after the log is physically durable does the commit return success.

SQL
-- When Rajesh transfers money at the National Bank of India:
BEGIN;
UPDATE accounts SET balance = balance - 50000 WHERE account_id = 'SAV2001';
UPDATE accounts SET balance = balance + 50000 WHERE account_id = 'SAV2002';
COMMIT;

-- What happens internally:
-- 1. WAL record: "debit SAV2001 by 50000" → appended to WAL
-- 2. WAL record: "credit SAV2002 by 50000" → appended to WAL
-- 3. COMMIT record → appended to WAL
-- 4. fsync WAL to disk → WAL is physically durable
-- 5. Return "COMMIT" to the application
-- 6. Actual data pages updated LATER by the checkpointer
JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.sql.DataSource;

public class TransferService {

    private final DataSource dataSource;

    public TransferService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    // This transfer is durable because of WAL
    // Even if the server crashes after COMMIT returns,
    // the WAL ensures the transfer is recovered on restart
    public void transfer(String from, String to, BigDecimal amount)
            throws SQLException {
        try (Connection conn = dataSource.getConnection()) {
            conn.setAutoCommit(false);
            try {
                execute(conn, "UPDATE accounts SET balance = balance - ? "
                      + "WHERE account_id = ?", amount, from);
                execute(conn, "UPDATE accounts SET balance = balance + ? "
                      + "WHERE account_id = ?", amount, to);
                conn.commit();  // WAL fsynced before this returns
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            }
        }
    }

    private void execute(Connection conn, String sql, BigDecimal amount,
                         String accountId) throws SQLException {
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setBigDecimal(1, amount);
            ps.setString(2, accountId);
            ps.executeUpdate();
        }
    }
}

Why does WAL exist?

The problem WAL solves is the gap between 'the transaction said COMMIT' and 'the data pages are safely on disk.' Writing the actual data pages randomly across disk on every commit would be extremely slow, and a crash in the middle would corrupt the database. WAL decouples durability from the physical page writes.

DIAGRAM
Without WAL:
  COMMIT → write data page A (random I/O)
         → write data page B (random I/O)
         → crash between A and B!
         → database corrupted: A updated, B not

With WAL:
  COMMIT → append to WAL (sequential I/O, fast)
         → fsync WAL (durable)
         → return success
         → data pages written LATER by checkpointer
         → crash at any point? replay WAL to recover

Sequential log appends are much faster than random data page writes. The actual data pages are flushed to disk later by the background checkpointer process.

How does crash recovery work?

On restart after a crash, PostgreSQL reads the WAL from the last checkpoint forward and replays every committed change against the data files. Uncommitted transactions are discarded.

DIAGRAM
Crash recovery timeline:

Checkpoint at 10:00 → data pages flushed to disk
Transaction A commits at 10:05 → WAL record durable
Transaction B commits at 10:08 → WAL record durable
Transaction C in progress at 10:10 → WAL record but no COMMIT
CRASH at 10:10

Recovery:
1. Read WAL from checkpoint (10:00) forward
2. Replay Transaction A (committed) → redo
3. Replay Transaction B (committed) → redo
4. Transaction C (no COMMIT record) → discard
5. Database is consistent: A and B applied, C rolled back
JAVA
package com.bank.admin;

public class CrashRecoveryExplanation {

    // After a crash, PostgreSQL automatically recovers:
    // 1. Finds the last checkpoint in pg_control
    // 2. Reads WAL from that checkpoint forward
    // 3. Replays all committed transactions
    // 4. Discards uncommitted transactions
    // 5. Database is ready for connections

    // The application does NOT need to do anything special
    // WAL guarantees that committed transactions survive crashes
    // This is why ACID's Durability guarantee holds
}

How does WAL enable replication?

A replica is simply another server replaying the same WAL stream. The primary ships WAL records to replicas, which apply them to stay in sync. This is streaming replication.

DIAGRAM
Streaming replication:

┌──────────────┐    WAL stream    ┌──────────────┐
│   Primary    │─────────────────→│   Replica    │
│ (writes)     │                  │ (reads)      │
│              │                  │              │
│ WAL: A,B,C   │  ──stream──→    │ Apply: A,B,C │
│ Data pages   │                  │ Data pages   │
└──────────────┘                  └──────────────┘

The replica's data is identical to the primary's
because it applies the same WAL records.
Read queries can be sent to the replica to offload the primary.
SQL
-- On the primary: configure streaming replication
-- postgresql.conf:
-- wal_level = replica
-- max_wal_senders = 3
-- archive_mode = on
-- archive_command = 'cp %p /archive/%f'

-- On the replica: start in recovery mode
-- recovery.conf / standby.signal:
-- primary_conninfo = 'host=primary.nbi.internal port=5432'
-- restore_command = 'cp /archive/%f %p'

What is point-in-time recovery (PITR)?

PITR is the heart of a ledger backup strategy. You take a base backup (a full physical copy of the data directory) and continuously archive every WAL segment. To recover, you restore the base backup and replay archived WAL up to any chosen timestamp.

DIAGRAM
Point-in-time recovery:

Base backup taken at Monday 00:00
  [Full copy of data directory → stored on S3]

WAL segments archived continuously:
  Mon 00:00 → Mon 06:00 → Mon 12:00 → Mon 18:00 → Tue 00:00 ...
  [Each segment → stored on S3]

Bad batch job runs at Tuesday 10:00, corrupts balances

Recovery:
  1. Restore base backup (Monday 00:00)
  2. Replay WAL from Monday 00:00 to Tuesday 09:59:59
  3. Stop replay at 09:59:59 (one second before the bad batch)
  4. Every good transaction from Monday and Tuesday morning is preserved
  5. The bad batch is excluded
SQL
-- Take a base backup
SELECT pg_start_backup('nbi_weekly_backup');
-- Copy data directory to S3
SELECT pg_stop_backup();

-- Or use pg_basebackup utility:
-- pg_basebackup -h primary.nbi.internal -D /backup/base -P

-- Recover to a specific point in time:
-- In recovery.conf / postgresql.conf:
-- restore_command = 'aws s3 cp s3://nbi-wal-archive/%f %p'
-- recovery_target_time = '2024-06-11 09:59:59'
-- recovery_target_action = 'promote'

How does WAL shape the backup strategy for a banking ledger?

A ledger must be auditable and must never lose a committed entry. The backup strategy is therefore: periodic base backups plus continuous WAL archiving to durable storage.

JAVA
package com.bank.admin;

public class BackupStrategy {

    // National Bank of India backup strategy:
    //
    // 1. Weekly base backup (Sunday 02:00)
    //    pg_basebackup → S3 (nbi-backups/base/)
    //
    // 2. Continuous WAL archiving
    //    archive_command → S3 (nbi-backups/wal/)
    //    Every filled WAL segment (16MB) is archived immediately
    //
    // 3. Recovery Point Objective (RPO): ~0
    //    Every committed transaction is in an archived WAL segment
    //    Maximum data loss = transactions since last archived segment
    //    With synchronous commit: truly zero data loss
    //
    // 4. Recovery Time Objective (RTO): depends on WAL to replay
    //    More frequent base backups = less WAL to replay = faster recovery
    //    Weekly base + 7 days of WAL vs Daily base + 1 day of WAL
    //
    // 5. Test restores monthly
    //    An untested backup is a hope, not a strategy
}

What are checkpoints and why do they matter?

Checkpoints bound how much WAL must be replayed on recovery. During a checkpoint, PostgreSQL flushes all dirty data pages to disk, creating a known-good state. Recovery starts from the last checkpoint.

TEXT
Checkpoint frequency trade-off:

Frequent checkpoints (every 5 minutes):
  + Faster crash recovery (less WAL to replay)
  - More write amplification (more dirty pages flushed)
  - Higher I/O load during checkpoints

Infrequent checkpoints (every 30 minutes):
  + Less write amplification
  + Lower steady-state I/O
  - Slower crash recovery (more WAL to replay)

For a banking ledger:
  Moderate frequency (every 10-15 minutes)
  checkpoint_completion_target = 0.9 (spread I/O over the interval)
SQL
-- PostgreSQL checkpoint configuration
-- postgresql.conf:
-- checkpoint_timeout = '10min'        -- max time between checkpoints
-- max_wal_size = '1GB'                -- checkpoint if WAL exceeds this
-- checkpoint_completion_target = 0.9  -- spread I/O over 90% of interval

-- Monitor checkpoints:
SELECT * FROM pg_stat_bgwriter;
-- checkpoints_timed:  how many triggered by timeout
-- checkpoints_req:    how many triggered by WAL size
-- buffers_checkpoint:  pages written during checkpoints

What are common mistakes with WAL and backups?

The most dangerous mistake is not monitoring WAL archiving. A gap in archived segments breaks the PITR chain.

TEXT
-- WRONG: archive_command fails silently
-- WAL segment 00000001000000010000002A was not archived
-- PITR chain is broken: cannot recover past that point

-- CORRECT: monitor archive status
SELECT * FROM pg_stat_archiver;
-- last_archived_wal, last_failed_wal, failed_count
-- Alert if failed_count > 0 or if last_archived_wal is stale

-- CORRECT: test restores regularly
-- An untested backup is a hope, not a strategy
JAVA
package com.bank.monitoring;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;

public class WalArchiveMonitor {

    private final DataSource dataSource;

    public WalArchiveMonitor(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void checkArchiveHealth() throws SQLException {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(
                     "SELECT last_archived_wal, last_failed_wal, failed_count "
                   + "FROM pg_stat_archiver")) {
            if (rs.next()) {
                int failedCount = rs.getInt("failed_count");
                if (failedCount > 0) {
                    System.err.println("WAL ARCHIVE ALERT: "
                            + failedCount + " failed archives!");
                    System.err.println("Last failed: "
                            + rs.getString("last_failed_wal"));
                }
            }
        }
    }
}

What should a senior engineer know?

A senior engineer should understand synchronous vs asynchronous commit. Synchronous commit (the default) waits for WAL to be fsynced before returning COMMIT, guaranteeing durability. Asynchronous commit returns faster but risks losing the last few transactions if the server crashes.

SQL
-- Synchronous commit (default, safe for banking)
SET synchronous_commit = on;
-- COMMIT waits for fsync → zero data loss on crash

-- Asynchronous commit (faster, risk of recent loss)
SET synchronous_commit = off;
-- COMMIT returns before fsync → last ~200ms of commits may be lost
-- Acceptable for non-critical data like activity logs

Visual Overview

DIAGRAM
┌────────────────────────────────────────────────────────────┐
│              Write-Ahead Logging (WAL)                    │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  Transaction: Transfer 50,000 from SAV2001 to SAV2002    │
│                                                            │
│  Step 1: Write WAL records (sequential, fast)             │
│  ┌─────────────────────────────────────────────┐          │
│  │ WAL: [debit SAV2001 -50000] [credit +50000] │          │
│  │ WAL: [COMMIT]                                │          │
│  │ → fsync to disk (durable!)                   │          │
│  └─────────────────────────────────────────────┘          │
│                                                            │
│  Step 2: Return COMMIT to application                     │
│                                                            │
│  Step 3: Data pages updated LATER (background)            │
│  ┌─────────────────────────────────────────────┐          │
│  │ Checkpointer flushes dirty pages to disk    │          │
│  │ (happens asynchronously, not blocking)       │          │
│  └─────────────────────────────────────────────┘          │
│                                                            │
│  Crash recovery:                                          │
│  WAL checkpoint → replay committed → discard uncommitted  │
│                                                            │
│  Backup strategy:                                         │
│  [Base backup] + [Archived WAL] → PITR to any moment     │
└────────────────────────────────────────────────────────────┘

Key Takeaway

Write-Ahead Logging is the durability protocol at the heart of PostgreSQL. Before any data page is modified, the database first appends a compact record of the intended change to a sequential log and fsyncs that log to disk. Only after the log is physically durable does the commit return success. The actual data pages are written later by the background checkpointer. This gives durability without the cost of random writes on every commit.

WAL provides three capabilities. Crash recovery: if the server dies, on restart it replays the WAL from the last checkpoint, redoing committed changes and discarding uncommitted ones, so a committed transaction is never lost. Replication: a replica is simply another server replaying the same WAL stream, which is how streaming read replicas stay in sync. And point-in-time recovery: by taking a base backup and continuously archiving every WAL segment, you can restore to any chosen timestamp.

That last capability shapes the entire backup strategy of a banking ledger. A ledger must be auditable and must never lose a committed entry, so the strategy is periodic base backups plus continuous WAL archiving to durable storage like S3. This gives essentially zero committed-data loss and surgical recovery to any moment, not just last night's snapshot.

At the National Bank of India, the ledger ran with synchronous commit so WAL was fsynced before any transfer was acknowledged. WAL was archived continuously to S3 with weekly base backups. When a faulty batch job at 10:00 AM corrupted balances, Pankaj restored the latest base backup and replayed archived WAL to 9:59:59 AM, recovering every good transaction from earlier that morning while excluding the bad batch.

Remember one sentence: log the change durably first and update the data pages after, so a crash is recovered by replaying the log, and a base backup plus archived WAL lets you restore to any moment.

---

Share