WAL Saturation
criticalSummary
PostgreSQL WAL (Write-Ahead Log) generation rate exceeds wal_buffers flush capacity or downstream replica/WAL archive bandwidth, causing write transactions to stall waiting for WAL flush and replication lag to grow unboundedly.
Description
Every write operation in PostgreSQL generates WAL records. WAL is the authoritative record of all changes; a transaction cannot commit until its WAL records are durably flushed to disk. WAL generation saturates when the rate of write operations produces more WAL bytes per second than the WAL disk can absorb at the required latency.
The WAL pipeline has multiple potential saturation points. First, wal_buffers: shared memory buffers that hold WAL records before fsync. The default is -1, which auto-sizes wal_buffers to roughly 1/32 of shared_buffers, bounded between a minimum of 64kB and a maximum of one WAL segment (16MB by default); this auto-sizing default has been stable since PostgreSQL 9.1, and an explicit numeric setting overrides it. If transactions generate WAL faster than the background WAL writer flushes wal_buffers, transactions block waiting to write WAL. Second, WAL disk I/O: the WAL disk must sustain the WAL write bandwidth plus fsync IOPS. For a workload generating 500MB/s WAL (1 million 500-byte updates/second), the WAL device must sustain 500MB/s sequential write throughput: a GP3 EBS volume's max is 1,000 MB/s (125 MB/s baseline), so this workload can run but a GP2 volume would saturate. Third, WAL archiving: pg_wal_archive must copy WAL segments to S3, NFS, or another target faster than segments are generated. If archiving falls behind, pg_wal fills with unarchived segments and can exhaust disk space.
For streaming replication, WAL saturation manifests as replication lag: the replica WAL receiver cannot apply WAL faster than it arrives. At 500MB/s WAL generation and a replica with 100MB/s network bandwidth, replication lag grows at 400MB/s : 1.44TB/hour. The replica cannot catch up until the primary write rate drops below the replica network bandwidth.
The failure is particularly dangerous for CDC systems (Debezium, logical replication slots): a lagging replication slot retains WAL on the primary's pg_wal directory. If the consumer falls behind, pg_wal grows without bound until it fills the primary's disk: at which point the primary will not be able to generate new WAL and will halt all write operations.
PostgreSQL's WAL design is a single stream doing two jobs at once: the same records that let a crashed instance replay to a consistent state are also what streaming replication ships to standbys. MySQL/InnoDB splits this into two independent logs. The InnoDB redo log records physical page changes for crash recovery only, sized via innodb_redo_log_capacity (MySQL 8.0.30 and later; earlier versions size it via innodb_log_file_size times innodb_log_files_in_group). The binary log (binlog) is a separate, logical record of statements or row changes used for replication and point-in-time recovery, controlled independently (max_binlog_size, sync_binlog, binlog_expire_logs_seconds). These are not the same log: they are written by different code paths and each can independently become the bottleneck. A redo log undersized for the write rate forces synchronous checkpointing that stalls commits; a binlog that replicas or CDC consumers (Debezium reading the binlog, not the redo log) cannot keep up with grows lag the same way a lagging PostgreSQL replication slot does, but it is a different file with different retention controls than the redo log.
LSM engines take a narrower approach again. Cassandra's commit log, like RocksDB's write-ahead log, exists only to make the current unflushed memtable durable across a crash; once the memtable is flushed to an SSTable, the corresponding commit log segments are no longer needed for recovery and are recycled. Neither log is what carries data to other replicas. Cassandra replicates at write time through its own replication protocol (the coordinator writes to each replica node responsible for the key, per the keyspace's replication factor), not by shipping the commit log downstream, so commit log pressure and replication lag are separate concerns there in a way they are not under PostgreSQL's single-stream design.
Characteristics
Triggers
- ·Bulk data import or ETL generating WAL at storage bandwidth ceiling (>200MB/s)
- ·High UPDATE rate on wide tables with many indexes (high WAF producing large WAL records)
- ·full_page_writes = on generating full 8KB page images to WAL after each checkpoint
- ·Logical replication consumer falling behind, causing WAL retention to accumulate on disk
- ·WAL archive destination slow or unavailable, causing pg_wal directory to fill
Detection Signals
Mitigation Strategies
An idle or lagging replication slot is the most common cause of unconstrained WAL accumulation. SELECT slot_name, pg_size_pretty(pg_wal_lsn_diff( pg_current_wal_lsn(), confirmed_flush_lsn)) AS lag FROM pg_replication_slots. Drop slots for consumers that have been inactive >24 hours.
In PostgreSQL 13+, max_slot_wal_keep_size limits how much WAL a lagging slot can retain. When the limit is reached, the slot is invalidated rather than filling the disk. Set to 10–50GB based on disk headroom. Trade-off: invalidated slots lose their position and must restart from scratch.
For bulk imports or ETL jobs, add rate limiting (INSERT N rows, sleep M ms). At 10,000 rows/batch with 10ms sleep: ~1M rows/minute. This bounds WAL generation to a rate that replicas and WAL archive can sustain.
Place pg_wal on a dedicated NVMe volume or high-IOPS EBS io2 volume, separate from data files. WAL I/O is sequential; NVMe achieves 3–7 GB/s sequential writes, far exceeding EBS limits. Data I/O and WAL I/O no longer compete.
Recovery Steps
- 1.Check pg_replication_slots for lagging slots: any slot with >5GB retained WAL is an immediate risk
- 2.Drop lagging slots that belong to offline or slow consumers
- 3.If pg_wal is filling disk: pg_switch_wal() to force WAL segment rotation, then check archive_status
- 4.Throttle any active bulk write workload to reduce WAL generation rate
- 5.Monitor pg_stat_replication.write_lag on all replicas: should decrease after load reduction
- 6.Post-recovery: implement max_slot_wal_keep_size and add pg_wal disk space monitoring
Estimated recovery time: Seconds to minutes once lagging replication slots are dropped and bulk writes are throttled. Replica lag recovery depends on how far behind replicas have fallen (1GB lag at 100MB/s replication bandwidth takes 10s; 1TB lag takes ~3 hours). Disk space recovery from dropped slots is near-immediate.
Affected Systems
Patterns
Technologies
Basis
Precisely specified PostgreSQL WAL internals with measurable parameters; replication slot WAL accumulation is a well-documented production hazard. The MySQL redo log/binlog split and the Cassandra/RocksDB commit-log scope are standard documented mechanisms; exact InnoDB redo log sizing knobs are version-scoped in the text and should be re-checked against the deployed MySQL version.
Sources & Claims
PostgreSQL wal_buffers defaults to -1, which auto-sizes it to roughly 1/32 of shared_buffers, bounded between 64kB and one WAL segment (16MB by default); this auto-sizing default has been in place since PostgreSQL 9.1
pendingofficial documentation · PostgreSQL documentation, Write Ahead Log configuration (wal_buffers)
storage-engine-internals-spine batch 3
MySQL 8.0.30 replaced innodb_log_file_size and innodb_log_files_in_group with the single innodb_redo_log_capacity parameter for sizing the InnoDB redo log; earlier 8.0.x and 5.7 versions size the redo log via the two older parameters
pendingofficial documentation · MySQL 8.0 Reference Manual, InnoDB Redo Log
storage-engine-internals-spine batch 3
PostgreSQL uses a single WAL stream for both crash recovery and streaming replication, while MySQL/InnoDB uses two independent logs: the redo log for crash recovery and the binary log (binlog) for replication and point-in-time recovery, with independent retention controls
pendingofficial documentation · PostgreSQL WAL documentation; MySQL InnoDB Redo Log and Binary Log documentation
storage-engine-internals-spine batch 3
Cassandra's commit log provides crash recovery for data not yet flushed from the memtable and is not itself used for inter-node replication, which Cassandra performs via its own replication protocol at write time
pendingofficial documentation · Apache Cassandra documentation, Commit log
storage-engine-internals-spine batch 3
Run This Failure
Blast radius analysis for this failure mode within each scenario that carries it.
Related Architecture Knowledge
Inbound: affects this entity
Write-heavy transactional workloads generate high WAL volume that can saturate WAL writer throughput, fill the WAL buffer, and: in the extreme: cause write transactions to block waiting for WAL to be flushed to disk or consumed by replicas.
Tradeoffs
- ·Increasing wal_buffers improves burst write performance but consumes more shared memory
- ·synchronous_commit=off reduces WAL durability window (last 200-400ms of commits unconfirmed on crash)
- ·Logical replication slots (CDC) hold WAL longer than streaming replication: additional WAL retention risk
Used In Architecture Scenarios
Financial Ledger
An append-only audit log architecture for capturing system actions: user operations, data access events, configuration changes, and financial operations: with cryptographic integrity chaining, immutable storage, and dual-path query serving. PostgreSQL stores the authoritative append-only event log in time-partitioned tables; ClickHouse serves historical aggregate queries and retention analytics; Kafka streams audit events in real time to SIEM integrations and security dashboards; Redis caches hot audit query results for compliance-facing read paths. No record is ever updated or deleted: only inserts are permitted. Each record includes a hash of the previous record, forming a tamper-evident chain verifiable without external tooling.
Write-Heavy Application
A high-rate device telemetry ingestion architecture designed for millions of devices emitting metrics at 1–60 second intervals. Kafka absorbs device writes as an ingestion buffer, decoupling device-facing ingest endpoints from the storage write path so that downstream storage pressure never propagates back to devices. TimescaleDB provides time-series storage with automatic chunk partitioning by time range, native compression, and continuous aggregate views for rollup queries. ClickHouse serves as the OLAP layer for device fleet analytics queries. Redis caches last-known device state (current readings per device) for real-time alerting queries that must not scan historical storage. Backpressure on the Kafka consumer side prevents storage write throughput from being overwhelmed by burst ingestion events from device reconnect storms.
Analytics Pipeline
A metrics, logs, and traces ingestion and query platform built to absorb the telemetry output of a production system fleet: including the telemetry volume spikes that accompany the incidents the platform is meant to detect. ClickHouse stores metrics data with automatic time-based rollup via continuous materialized views; TimescaleDB provides complementary time-series storage for high-cardinality alert evaluation; Kafka buffers the ingestion stream against downstream write pressure, decoupling ingest acceptance rate from storage write throughput; Elasticsearch serves log full-text search and structured field filtering; Redis caches dashboard query results and active alert state for sub-100ms alert evaluation latency. The alert engine evaluates threshold and anomaly rules against pre-computed materialized views, not raw data, to bound alert evaluation cost independent of ingestion volume.
Write-Heavy Application
A high-volume transactional write architecture anchored on PostgreSQL, where write throughput, durability guarantees, and audit completeness must coexist. The outbox pattern ensures reliable event publishing to Kafka without two-phase commit, and WAL-based CDC provides a durable change log that can reconstruct system state. Connection pooling via PgBouncer bounds connection overhead at the database layer.