PostgreSQL Role Transition Management#
CloudNativePG manages PostgreSQL promotion and demotion through the instance manager (running inside each pod) in coordination with the operator. The two core workflows are switchover (planned, operator-initiated) and failover (unplanned, triggered by primary loss). Both converge on the same instance-level primitives but take different orchestration paths.
Key Files#
| File | Purpose |
|---|---|
pkg/management/postgres/instance.go | Demote(), Rewind(), SetPostgreSQLAutoConfWritable() |
pkg/management/postgres/configuration.go | UpdateReplicaConfiguration(), configurePostgresOverrideConfFileForRewind(), writePostgresOverrideConfFile() |
pkg/management/postgres/promote.go | PromoteAndWait() |
internal/management/controller/instance_startup.go | verifyPgDataCoherenceForPrimary() — startup gate that triggers pg_rewind + demotion |
internal/controller/cluster_upgrade.go | updatePrimaryPod() — operator-side switchover orchestration |
internal/controller/replicas.go | Failover target selection |
Promotion (Replica → Primary)#
Promotion is performed by PromoteAndWait() which:
- Runs
pg_ctl promote -wto trigger PostgreSQL promotion. - Polls until
standby.signalis removed from PGDATA (the canonical PostgreSQL signal that the instance is no longer a standby). - Issues a
CHECKPOINT— required so thatpg_rewindon the old primary can find a consistent divergence point.
Known gap:
PromoteAndWait()does not callpg_switch_wal()after promotion. If the promotion LSN falls mid-segment, that WAL segment contains pages from two timelines. Backups whose redo location lands in this mixed segment will fail to restore. See issue #10422 for details and workaround.
Demotion (Primary → Replica)#
On an old primary that has been superseded, verifyPgDataCoherenceForPrimary() runs at startup and drives the full demotion sequence :
- Wait for
CurrentPrimary == TargetPrimaryin cluster status — confirms the new primary is recognized. - Wait for the new primary to accept connections (
WaitForPrimaryAvailable). - Clean up any stale PID file.
- Set
postgresql.auto.confwritable (mode0600) sopg_rewindcan write to it — it's normally locked to0400. - Archive all pending WAL via
ArchiveAllReadyWALs— prevents losing WAL before the rewind. - Run
pg_rewindto realign the data directory with the new primary. - Call
Demote()to finalize replica configuration.
Demote() is a thin wrapper that calls UpdateReplicaConfiguration(), which writes override.conf and creates standby.signal .
Configuration File Management#
CNPG uses two operator-managed config files alongside PostgreSQL's standard files:
override.conf#
Written by writePostgresOverrideConfFile(), this file always contains :
restore_command— WAL restore via the instance managerrecovery_target_timeline = latestprimary_conninfo— connection string to the upstream primaryprimary_slot_name— HA replication slot name (when non-empty)
During pg_rewind, configurePostgresOverrideConfFileForRewind() replaces override.conf with a rewind-specific variant that passes --rewind to wal-restore, disabling WAL prefetching and end-of-stream detection. After a successful rewind, Demote() replaces it with the standard replica configuration .
postgresql.auto.conf#
PostgreSQL's ALTER SYSTEM writes go here. CNPG locks this file to read-only (0400) to prevent user-initiated ALTER SYSTEM calls from conflicting with operator-managed settings. Permissions are temporarily relaxed to 0600 before pg_rewind runs, then restored by the reconciliation loop. This behavior is version-aware (active only for PostgreSQL < 17) .
Historical note: Before v1.21, replication settings were stored in
postgresql.auto.conf. AmigratePostgresAutoConfFile()function moved them tooverride.conf. This migration shim was removed in PR #9965 (merged Feb 2026) after all clusters had sufficient time to upgrade .
custom.conf (operator-managed GUCs)#
The operator writes cluster GUC settings to the custom configuration file (PostgresqlCustomConfigurationFile) via RefreshConfigurationFilesFromCluster() . This is separate from override.conf and is reconciled on every configuration change — it is not role-sensitive.
Replication Slot Persistence Across Role Changes#
HA Physical Slots (always enabled)#
CNPG creates a physical HA slot per cluster instance on the primary (named _cnpg_<instance>). On standbys, a background runner synchronizes slots from the primary: creating, updating (advancing restart_lsn), and deleting stale slots. Slots that hold xmin are specifically deleted from demoted-primaries to prevent VACUUM blocking.
The synchronizeReplicationSlots function was previously a first-error-aborts-all loop. Issue #11113 documented that a single failing slot blocked synchronization and cleanup of every other slot on a standby; a fix (PR #11119) changes this to continue processing remaining slots .
Logical Slot Synchronization (SynchronizeLogicalDecoding)#
When spec.replicationSlots.highAvailability.synchronizeLogicalDecoding: true is set, CNPG synchronizes logical replication slots across standbys (requires PostgreSQL 17+). The primary includes the synchronized slot names in synchronous_standby_names configuration generated in createPostgresqlConfiguration() .
Post-switchover orphan bug (issue #9969): After a switchover, the demoted primary retains logical slots with synced=false. PostgreSQL 17's slot sync worker refuses to overwrite these, so they accumulate WAL indefinitely. The proposed fix drops slots matching synced=false AND failover=true AND active=false. Workaround: manually drop the orphaned slot on the replica :
SELECT pg_drop_replication_slot(slot_name)
FROM pg_replication_slots
WHERE slot_name = '<SLOT_NAME>'
AND synced = false
AND pg_is_in_recovery();
Startup Coherence Check#
verifyPgDataCoherenceForPrimary() runs at every instance manager startup. It detects three states :
| State | Action |
|---|---|
cluster.IsReplica() and PGDATA shows primary | Call Demote() immediately (hibernated cluster demoted) |
TargetPrimary == self | Assert primary; write CurrentPrimary if first boot |
| Otherwise (old primary, switchover in progress) | Wait → archive WAL → pg_rewind → Demote() |
Known Issues Summary#
| Issue | Description | Status |
|---|---|---|
| #10422 | Mixed-timeline WAL segment after promotion causes backup restore failures | Open; no pg_switch_wal() after promote |
| #9969 | Orphaned logical slots on demoted primary cause WAL accumulation | Fix pending (PR in review) |
| #11113 | One failing slot blocks sync and cleanup of all other slots on standby | Fix in PR #11119 |