Replication
Replication slots
A replication slot tracks how far a physical or logical consumer has progressed and normally retains required WAL, subject to configured retention and idle-slot limits that can invalidate the slot.
In plain words
Without a slot, the primary deletes old WAL files at each checkpoint once it no longer needs them itself. A standby that was offline for an hour may come back and find the WAL it needs is already gone.
A replication slot is a durable progress marker for a consumer. By default PostgreSQL retains WAL needed from the slot's restart_lsn, which can allow pg_wal to grow severely if the consumer disappears. With max_slot_wal_keep_size or idle_replication_slot_timeout, PostgreSQL can instead invalidate an unusable or idle slot rather than retain WAL without bound.
The safety nets are max_slot_wal_keep_size (give up on a slot once it holds too much WAL) and, new in PostgreSQL 18, idle_replication_slot_timeout (give up on a slot that has been unused for too long). Disconnect the consumer below and watch the disk.
For DBAs and operators
What a slot holds back
- WAL: segments from
restart_lsnon are kept. Physical slots advance it with the standby's flush position; logical slots advance it only as far as the oldest transaction still being decoded. - Rows: a physical slot with
hot_standby_feedbackstores the standby'sxmin, which stops VACUUM from removing dead rows in all tables and holds back XID freezing; a logical slot always has acatalog_xmin, which does the same for the system catalogs. Both stay in force even while the slot is inactive.
Monitoring
SELECT slot_name, slot_type, active, wal_status, safe_wal_size,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained,
inactive_since, invalidation_reason, xmin, catalog_xmin
FROM pg_replication_slots;wal_status is reserved (within max_wal_size), extended (beyond it but still kept, by the slot or wal_keep_size), unreserved (past max_slot_wal_keep_size: the next checkpoint removes it unless the consumer catches up) or lost. safe_wal_size is how many more bytes can be written before the slot becomes lost; NULL when the limit is -1. inactive_since exists since PostgreSQL 17.
Invalidation
The checkpointer invalidates slots that exceed the limits (real PostgreSQL 18 messages):
LOG: invalidating obsolete replication slot "standby1"DETAIL: The slot's restart_lsn 2/1C000000 exceeds the limit by 23068672 bytes.DETAIL: The slot's idle time of 3601s exceeds the configured "idle_replication_slot_timeout" duration of 3600s.An invalidated slot shows invalidation_reason wal_removed or idle_timeout (others: rows_removed, wal_level_insufficient for logical slots on a standby). It cannot be used again: drop it and rebuild the standby or re-sync the subscription. idle_replication_slot_timeout is checked at checkpoints, does not apply to synced slots on a standby (synced = true), and is off (0) by default.
When nobody set a limit
PANIC: could not write to file "pg_wal/xlogtemp.12345": No space left on deviceThe server goes down, and it cannot run normally again until space is freed. Never delete files from pg_wal by hand: free space elsewhere or grow the volume, start the server, then SELECT pg_drop_replication_slot('standby1') and let the next checkpoint remove the WAL.
Good practice: set max_slot_wal_keep_size to what the WAL volume can afford, alert on wal_status <> 'reserved' and on active = false, and drop slots of decommissioned replicas. Since PostgreSQL 17 logical failover slots can be synchronized to standbys (sync_replication_slots).
For developers: inside the source
Where it lives
slot.candslot.h: creation, acquisition, persistence, horizon computation and invalidation. SQL functions (pg_create_physical_replication_slot(),pg_replication_slot_advance(),pg_get_replication_slots()behind the view) are inslotfuncs.c; failover-slot synchronization on a standby is inslotsync.c(ReplSlotSyncWorkerMain(),synchronize_slots()).
Data structures
ReplicationSlotCtl->replication_slots[] is a shared array of max_replication_slots entries of type ReplicationSlot: runtime fields (in_use, active_pid, effective_xmin, effective_catalog_xmin, the logical candidate_* values, inactive_since) plus ReplicationSlotPersistentData data, which is what goes to disk: name, database, persistency (RS_PERSISTENT, RS_EPHEMERAL, RS_TEMPORARY), xmin, catalog_xmin, restart_lsn, confirmed_flush, invalidated, two_phase, failover, synced.
invalidated is a ReplicationSlotInvalidationCause: RS_INVAL_WAL_REMOVED, RS_INVAL_HORIZON, RS_INVAL_WAL_LEVEL, RS_INVAL_IDLE_TIMEOUT (the view shows wal_removed, rows_removed, wal_level_insufficient, idle_timeout). SaveSlotToPath() writes pg_replslot/<name>/state (via state.tmp, fsync, rename) from ReplicationSlotSave() and at every checkpoint in CheckPointReplicationSlots(); StartupReplicationSlots() → RestoreSlotFromDisk() reloads them.
How a slot holds things back
- WAL:
ReplicationSlotsComputeRequiredLSN()takes the minimumrestart_lsnand hands it toXLogSetReplicationSlotMinimumLSN(). At each checkpoint or restartpointKeepLogSeg()inxlog.cstops segment removal there, capped bymax_slot_wal_keep_size.GetWALAvailability()computeswal_status. - Rows:
ReplicationSlotsComputeRequiredXmin()aggregateseffective_xmin/effective_catalog_xminand callsProcArraySetReplicationSlotXmin(), so the procarray horizons that VACUUM uses include the slots. - Invalidation:
CreateCheckPoint()/CreateRestartPoint()callInvalidateObsoleteReplicationSlots()withRS_INVAL_WAL_REMOVED | RS_INVAL_IDLE_TIMEOUT;InvalidatePossiblyObsoleteSlot()→DetermineSlotInvalidationCause(). If the slot is in use it sendsSIGTERMto the owning walsender (or a recovery-conflict signal when run by the startup process), waits on the slot's condition variable, thenReportSlotInvalidation()logs "invalidating obsolete replication slot" with a DETAIL naming the cause.
Locking
ReplicationSlotAllocationLock (exclusive) serializes create and drop; ReplicationSlotControlLock protects scans of the array; each slot's mutex spinlock guards its fields, and io_in_progress_lock serializes writing its state file. A process owns at most one slot at a time (MyReplicationSlot, via ReplicationSlotAcquire()/ReplicationSlotRelease()).
Experiment
- TAP tests in
src/test/recovery/t/:019_replslot_limit.pl(max_slot_wal_keep_size,wal_statustransitions),044_invalidate_inactive_slots.pl(idle_timeout, driven by an injection point),040_standby_failover_slots_sync.pl. - Combine
pg_ls_waldir()withpg_replication_slots.restart_lsnto see which segments a slot pins.
Parameters that shape this
max_replication_slots max_slot_wal_keep_size idle_replication_slot_timeout wal_keep_size max_wal_size primary_slot_name hot_standby_feedback sync_replication_slots synchronized_standby_slots
Related topics
Streaming replication Logical replication WAL sender Checkpoints Write-ahead log (WAL) Hot standby and recovery conflicts XID wraparound and freezing