Postgres stores tables and indexes in binary files on disk. Those files are split into fixed-size blocks, usually called pages. A page is normally 8 KB, and it holds multiple rows plus some metadata. Postgres does not keep one readable text file per table.
So a page is the unit Postgres reads from disk into memory and later writes back. A row lives inside a page.
When a row changes, Postgres usually changes the copy of its page in shared memory first. That page is now dirty: memory has the newer version, while the table file on disk may still have the older version.
How can Postgres acknowledge my transaction before that dirty page reaches the table file safely? This is where WAL comes in.
WAL means Write-Ahead Log. It is an ordered sequence of records describing changes needed to redo database work. The important rule is in the name: the WAL record must reach durable storage before the changed data page is allowed to reach durable storage.
For a normal durable commit (synchronous_commit = on), my mental model is:
So commit does not mean every changed table page is already on disk. It means the WAL needed to recover the committed transaction is durable. If Postgres crashes before the dirty page is written, startup recovery replays WAL and rebuilds the missing change.
WAL is append-oriented, which is friendlier to storage than forcing many scattered data pages to disk for every commit. But WAL is more than a performance trick; its first job is durability and crash recovery.
PostgreSQL describes the write-ahead rule here.
CDC means Change Data Capture. Instead of repeatedly asking every table, “what changed?”, a consumer follows the database's change stream.
Raw WAL describes physical storage-level changes and depends on Postgres internals. An application usually wants a logical event such as:
orders: UPDATE id=42, status='paid'
Logical decoding reads WAL and turns the relevant records into a logical stream of table changes. An output plugin decides the actual format sent to the consumer. So logical decoding does not literally convert every WAL record into one row; it reconstructs transaction-level changes into a form a consumer can understand.
Three pieces fit together:
A slot is closer to a durable cursor than a pointer to one WAL file. If the consumer stops, required WAL can accumulate and fill the disk. This means replication-slot lag must be monitored.
There are two related paths:
The logical decoding documentation explains decoding, slots, and output plugins.
First I can check the current WAL level:
SHOW wal_level;
-- or
SELECT name, setting
FROM pg_settings
WHERE name IN ('wal_level', 'rds.logical_replication');
If wal_level is replica, Postgres has enough WAL for physical replication, but not the extra information required for logical decoding. Logical CDC needs wal_level = logical.
On Amazon RDS for PostgreSQL, I do not edit postgresql.conf directly. I attach a custom DB parameter group, set:
rds.logical_replication = 1
This is a static setting, so the DB instance must be rebooted for it to take effect. RDS also needs suitable values for max_replication_slots, max_wal_senders, and max_connections. After the reboot, SHOW wal_level; should return logical.
Then I create the publication and/or logical slot required by the CDC consumer, and give the consumer a user with the necessary replication and table permissions. On RDS, setup generally involves the rds_superuser and rds_replication roles rather than a true PostgreSQL superuser.
One warning worth keeping next to the setup steps: enabling logical replication can generate more WAL, and an inactive slot can retain WAL indefinitely. Always watch slot lag and free storage.