Snowflake World Tour hits your city

See how leading teams deploy agents at scale. Find a stop near you. Register free.

How WAL Works

If Postgres crashes mid-transaction — power failure, kernel panic, out-of-memory kill — how does it guarantee that committed data isn't lost? The answer is WAL: Write-Ahead Logging.

The rule is simple: every change must be written to the WAL before it's written to the actual data files. When Postgres commits a transaction, it flushes the WAL record to disk. The data pages themselves can be written later, lazily, in the background. If the system crashes before those data pages hit disk, Postgres replays the WAL on startup and reconstructs the missing changes.

This design works because WAL writes are sequential — append-only to the end of a log file. Sequential I/O is dramatically faster than the random I/O required to update scattered data pages across the heap. It's the same principle behind every database's transaction log, but Postgres's implementation also serves double duty for replication and point-in-time recovery.

WAL Segments and LSNs

WAL is not a single growing file. It's divided into segments — 16 MB files by default — stored in the pg_wal/ directory of your data directory. Each segment is named with a combination of timeline ID and segment number in hexadecimal.

The current position in the WAL stream is identified by a Log Sequence Number (LSN) — a hexadecimal pointer in the format file/offset:

-- Where are we in the WAL stream right now?
SELECT pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 0/3A8B9D0

You can convert an LSN to the underlying WAL filename:

-- Which WAL file contains this LSN?
SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000010000000000000003

That filename breaks down as: timeline 00000001, segment 0000000000000003. The timeline changes on recovery or promotion (more on that later).

Checkpoints

WAL alone doesn't solve durability — you also need a way to limit how much WAL must be replayed after a crash. That's what checkpoints do.

A checkpoint flushes all dirty pages from shared buffers to the data files on disk, then writes a checkpoint record to the WAL. After a checkpoint, all WAL before that record is no longer needed for crash recovery — those segments can be recycled or archived.

The frequency of checkpoints is controlled by two settings: time and WAL volume. Whichever threshold is hit first triggers a checkpoint.

-- When was the last checkpoint?
SELECT
  checkpoints_timed,
  checkpoints_req,
  checkpoint_write_time,
  checkpoint_sync_time
FROM pg_stat_bgwriter;

checkpoints_timed is the count triggered by timeout; checkpoints_req is the count triggered by reaching max_wal_size. If checkpoints_req is climbing, your WAL volume is exceeding the configured size between checkpoints.

WAL Statistics

Postgres tracks WAL activity in the pg_stat_wal view:

SELECT
  wal_records,
  wal_fpi,
  wal_bytes,
  pg_size_pretty(wal_bytes) AS wal_written,
  wal_buffers_full,
  wal_write,
  wal_sync,
  stats_reset
FROM pg_stat_wal;
 wal_records |  wal_fpi  |   wal_bytes   | wal_written | wal_buffers_full | wal_write | wal_sync
-------------+-----------+---------------+-------------+------------------+-----------+----------
    48293012 |   1204501 |  6834019328   | 6364 MB     |             1204 |    412890 |   412890

Key columns: wal_fpi is full-page images (entire pages written to WAL after each checkpoint, to protect against torn writes). A high wal_buffers_full suggests wal_buffers is too small — WAL is being flushed before the buffer fills.

WAL Configuration

WAL behavior is controlled by a handful of settings in postgresql.conf. Getting these right matters for both performance and recoverability.

wal_level

The most important WAL setting determines how much information is recorded:

SHOW wal_level;
 wal_level
-----------
 replica
LevelWhat it recordsUse case
minimalOnly enough for crash recoveryStandalone servers with no replication or archiving
replicaEnough for streaming replication and archiving (default)Most production deployments
logicalAdds row-level change dataLogical replication, CDC, change capture

Changing wal_level requires a restart. If you need replication or point-in-time recovery — and you almost certainly do — leave it at replica or higher.

Checkpoint Tuning

SHOW checkpoint_timeout;
SHOW max_wal_size;
SHOW min_wal_size;
SHOW checkpoint_completion_target;
 checkpoint_timeout | max_wal_size | min_wal_size | checkpoint_completion_target
--------------------+--------------+--------------+-----------------------------
 5min               | 1GB          | 80MB         | 0.9
  • checkpoint_timeout (default: 5 minutes) — maximum time between automatic checkpoints. Longer intervals mean more WAL to replay on crash, but fewer checkpoint I/O spikes.
  • max_wal_size (default: 1 GB) — when cumulative WAL reaches this size since the last checkpoint, a new checkpoint is triggered. Not a hard limit — WAL can exceed this briefly during write bursts.
  • min_wal_size (default: 80 MB) — Postgres keeps at least this much recycled WAL on hand, ready for reuse. Avoids constant file creation/deletion overhead.
  • checkpoint_completion_target (default: 0.9) — spread checkpoint I/O over this fraction of the checkpoint interval. At 0.9, a 5-minute checkpoint_timeout spreads writes over 4.5 minutes instead of flushing everything at once.

For write-heavy workloads, increasing max_wal_size to 4-8 GB and checkpoint_timeout to 15-30 minutes reduces checkpoint frequency. The tradeoff: longer crash recovery time.

WAL Compression

SHOW wal_compression;

When set to on (or a specific algorithm like lz4 or zstd in PG 15+), Postgres compresses full-page images in WAL records. This reduces WAL volume — often by 30-50% — at the cost of some CPU. On I/O-bound systems, the net result is usually positive.

aside positive Snowflake Postgres handles WAL management, checkpointing, and archival automatically. WAL-based continuous backups enable point-in-time recovery with no manual configuration. You can focus on your application while the platform ensures durability and recoverability.

WAL Archiving and Point-in-Time Recovery

WAL segments are normally recycled after a checkpoint — the old data is gone. But if you archive those segments before recycling, you can combine them with a base backup to restore your database to any point in time. This is the foundation of Postgres disaster recovery.

Enabling WAL Archiving

# postgresql.conf
archive_mode = on
archive_command = 'cp %p /archive/wal/%f'

When archive_mode is enabled, Postgres calls archive_command for each completed WAL segment. The %p placeholder is the full path to the segment file; %f is just the filename. The command must return exit code 0 on success — Postgres will retry indefinitely until archiving succeeds.

In production, you'd replace cp with something that ships WAL to durable storage — S3, GCS, a remote server:

archive_command = 'aws s3 cp %p s3://my-bucket/wal/%f'

The Backup + WAL = PITR Equation

A base backup is a physical copy of the data directory at a moment in time. On its own, it restores to that exact moment. Combined with archived WAL, it restores to any moment between the backup and the most recent WAL segment.

Base Backup (taken Monday 2am)
  + Archived WAL segments (Monday 2am → Wednesday 4:37pm)
  = Restore to any second in that window

The key insight: a data directory copy without WAL is not a backup. If you snapshot the filesystem without ensuring WAL continuity, you have no guarantee of consistency or the ability to recover to a specific point.

Creating a Base Backup

pg_basebackup -D /backups/base_2025_01_15 -Ft -z -P -X stream

The -X stream flag ensures WAL generated during the backup is included. This gives you a self-contained starting point — the archived WAL from that point forward extends your recovery window.

Point-in-Time Recovery Workflow

To restore to a specific moment — say, just before an accidental DROP TABLE at 2025-01-15 14:32:00:

  1. Restore the most recent base backup before that time
  2. Configure recovery to stop at the target time
  3. Start Postgres — it replays WAL up to the target and stops
# postgresql.conf (or postgresql.auto.conf during recovery)
restore_command = 'cp /archive/wal/%f %p'
recovery_target_time = '2025-01-15 14:31:55'
recovery_target_action = 'promote'

Create the recovery.signal file in the data directory, then start Postgres. It will replay WAL from the base backup through to 14:31:55 and then promote to a normal read-write server.

Timeline IDs

Every time you perform a recovery, Postgres creates a new timeline — incrementing the timeline ID in subsequent WAL filenames. This prevents confusion between WAL generated before and after a recovery event.

Timeline history files (e.g., 00000002.history) record when and why a new timeline was created. They're essential for understanding which WAL sequence to follow when you have multiple recovery points or standby promotions in your history.

A higher timeline number doesn't necessarily mean "more recent data" — it means "a different recovery branch." When planning PITR, you need to know which timeline contains the data you want.

WAL and Replication

WAL isn't only for crash recovery. Postgres's replication system is built directly on top of WAL — standbys receive and replay the same WAL stream that a recovering server would use.

Streaming Replication

In streaming replication, a standby connects to the primary and receives WAL records in real time over a TCP connection. The standby replays them continuously, staying seconds (or less) behind the primary.

Requirements on the primary:

-- wal_level must be replica or logical
SHOW wal_level;

-- Reserve connection slots for replication
SHOW max_wal_senders;

-- Keep WAL available for slow standbys
SHOW wal_keep_size;
 wal_level | max_wal_senders | wal_keep_size
-----------+-----------------+---------------
 replica   | 10              | 1GB
  • max_wal_senders — how many concurrent replication connections the primary supports. Each standby uses one slot.
  • wal_keep_size — retain at least this much WAL even after checkpoint, so a standby that falls behind can catch up without needing archived WAL. Not a guarantee — under heavy write load, WAL can still be recycled.

Replication Slots

For guaranteed WAL retention, use replication slots:

-- Create a physical replication slot
SELECT pg_create_physical_replication_slot('standby_01');

-- Check slot status and retention
SELECT slot_name, active, restart_lsn,
       pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes
FROM pg_replication_slots;

A replication slot prevents Postgres from recycling any WAL that the consumer hasn't received yet. This guarantees the standby can always catch up — but it also means an inactive slot can cause unbounded WAL accumulation. Monitor retained_bytes and drop slots for decommissioned standbys.

Logical Replication

When wal_level = logical, Postgres decodes the physical WAL into logical row-level changes (INSERT, UPDATE, DELETE). This enables:

  • Replicating specific tables rather than the entire cluster
  • Replicating between different Postgres major versions
  • Feeding change data to external systems (CDC)

Logical replication is a large topic with its own guide — the key point here is that it's built on the same WAL infrastructure. The WAL is the single source of truth for all change propagation in Postgres.

For a deeper dive into streaming and logical replication setup, see the Replication Guide.

Conclusion

WAL is the foundation of everything reliable about Postgres. Durability, crash recovery, point-in-time restore, streaming replication, logical decoding — all of it flows from the same write-ahead log. Understanding WAL segments, LSNs, checkpoints, and archiving gives you the mental model to reason about backup strategies, replication lag, and recovery objectives. When something goes wrong at 3am, that mental model is what separates a confident recovery from a panicked one.

Related Resources

Official docs on WAL settings and tuning.


Understanding WAL file naming, LSNs, and segment management.


Timeline IDs and history files explained.


How WAL enables both streaming and logical replication.

Updated 2026-07-08

This content is provided as is, and is not maintained on an ongoing basis. It may be out of date with current Snowflake instances