DB Internals AtlasPostgreSQL 18.6 Home About

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_lsn on 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_feedback stores the standby's xmin, which stops VACUUM from removing dead rows in all tables and holds back XID freezing; a logical slot always has a catalog_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 device

The 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.c and slot.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 in slotfuncs.c; failover-slot synchronization on a standby is in slotsync.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 minimum restart_lsn and hands it to XLogSetReplicationSlotMinimumLSN(). At each checkpoint or restartpoint KeepLogSeg() in xlog.c stops segment removal there, capped by max_slot_wal_keep_size. GetWALAvailability() computes wal_status.
  • Rows: ReplicationSlotsComputeRequiredXmin() aggregates effective_xmin/effective_catalog_xmin and calls ProcArraySetReplicationSlotXmin(), so the procarray horizons that VACUUM uses include the slots.
  • Invalidation: CreateCheckPoint()/CreateRestartPoint() call InvalidateObsoleteReplicationSlots() with RS_INVAL_WAL_REMOVED | RS_INVAL_IDLE_TIMEOUT; InvalidatePossiblyObsoleteSlot() → DetermineSlotInvalidationCause(). If the slot is in use it sends SIGTERM to the owning walsender (or a recovery-conflict signal when run by the startup process), waits on the slot's condition variable, then ReportSlotInvalidation() 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_status transitions), 044_invalidate_inactive_slots.pl (idle_timeout, driven by an injection point), 040_standby_failover_slots_sync.pl.
  • Combine pg_ls_waldir() with pg_replication_slots.restart_lsn to 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