For as long as logical replication has existed in PostgreSQL, the rule has been: if you ever might want to use it, set wal_level = logical at server start and live with the WAL volume forever. The cost wasn’t disastrous, but it was always-on. Set it once, pay it forever, including for the 364 days a year you weren’t using a single logical slot.
PostgreSQL 19 changes this. The setting is still there. It just isn’t quite what you set anymore.
What actually changed
In PostgreSQL 19, wal_level becomes a floor, not the effective level. The server tracks an effective WAL level that is the maximum of the configured wal_level and whatever the current set of replication slots requires. Create the first logical slot on a server running with wal_level = replica and the effective level bumps to logical. Drop the last logical slot and the effective level drops back down.
This is exactly the behavior you’ve wanted for a decade, and it brings exactly the corner cases you should have expected.
The transition is not free
Logical decoding requires WAL records to carry information that replica simply doesn’t generate — most notably the old-tuple data needed to reconstruct UPDATEs and DELETEs against tables without primary keys, and the catalog snapshot information needed to interpret schema at decode time. You cannot retroactively make a WAL record carry data that wasn’t written into it.
So the slot creation can’t just flip a switch. It has to:
- Bump the effective level.
- Force a checkpoint so every writing backend has observed the change.
- Only then start the slot at an LSN guaranteed to have logical-level WAL behind it.
Until that completes, CREATE_REPLICATION_SLOT blocks. On a busy system, that “wait for the next checkpoint” can be tens of seconds. If you create slots from an automation script and your script has a one-second timeout, you have a new problem.
The downgrade direction is easier: when the last slot is dropped, the effective level drops at the next checkpoint. WAL written between “last slot dropped” and “next checkpoint” is still at logical level. No correctness issue, just minor wasted bytes.
Where this gets interesting
A few places need attention.
Cascading standbys. A physical standby replays WAL at whatever level the primary produced. That’s fine — physical replay doesn’t care about WAL level. But if you have a standby that also has logical slots (for offload), the effective level on the standby is independent of the primary’s slot state. Dropping all logical slots on the primary doesn’t help if the standby still has one. Confirm this in your environment before you assume the WAL volume drop will be symmetric.
Archived WAL. pg_control records the WAL level. If you ever try to start a logical decoder against an archived WAL stream, it needs logical-level records all the way back to the slot’s start LSN. With dynamic levels, your archive will be a mix. This isn’t broken — the per-segment metadata knows when each segment was written — but tooling that assumes “WAL level is constant” needs a look. If you have homegrown PITR-plus-logical-replay scripts, audit them.
Crash recovery. No change to correctness. The level recorded in pg_control reflects what was actually written. Recovery proceeds normally.
Prepared transactions. Two-phase commit interacts with logical decoding in known awkward ways. The dynamic-level mechanism doesn’t make this better, and probably doesn’t make it worse. If you use PREPARE TRANSACTION heavily, this is a place to test deliberately.
What to actually do
If you’re upgrading to PostgreSQL 19 and you’ve been running with wal_level = logical purely as insurance, drop it back to replica. The new mechanism does the right thing when you actually need logical. You’ll see meaningful WAL volume reduction on systems that don’t routinely run logical replication.
If you create logical slots from automation, raise your timeouts. The first slot creation on a previously-replica system is going to wait for a checkpoint, and you can’t make that go faster from outside the database.
If you’re maintaining tooling that assumes WAL level is fixed at server-lifetime granularity — and there is more such tooling out there than people realize — it needs updating.
The default is now to do the right thing. The right thing has corners.