WAL Archiving in CloudNativePG#
Overview#
CloudNativePG runs WAL archiving through the wal-archive subcommand of its instance manager binary (/controller/manager). PostgreSQL calls this command via archive_command for every WAL segment and history file that needs to be archived. The command dispatches to either a CNPG-I plugin (e.g., barman-cloud.cloudnative-pg.io) or the legacy Barman Cloud path, depending on cluster configuration.
Key entry points:
- CLI command:
internal/cmd/manager/walarchive/cmd.go - Core archiving logic:
pkg/management/postgres/archiver/archiver.go
Exit Code Signaling#
PostgreSQL interprets archive_command exit codes strictly: exit 0 = success (file is durably archived; PostgreSQL advances and never retries), non-zero = transient failure (PostgreSQL retries indefinitely).
All error paths in cmd.go return the error to Cobra, which causes os.Exit(1) via main.go . The error message is also pushed to the instance manager's status via SetWALArchiveStatusCondition() , which surfaces in kubectl cnpg status as Last Failed WAL.
| Condition | Exit Code | Behavior |
|---|---|---|
| Success | 0 | PostgreSQL marks file archived, moves on |
errSwitchoverInProgress | 1 | Logged as warning; PostgreSQL retries |
ErrMissingWALArchiverPlugin | 1 | BroadcastError() called → pod rollout |
| Other archive error | 1 | Logged as error; PostgreSQL retries |
Critical caveat — history files during switchover: The .history file for a new timeline is generated at the exact moment of promotion, which falls within the switchover window. The instance manager detects errSwitchoverInProgress and refuses to archive — but there is a known bug (#11219) where it returns exit 0 to PostgreSQL instead of non-zero . PostgreSQL considers the history file successfully archived and never retries it, leaving the object store permanently missing that .history file. All replicas that later need to follow the new timeline will crash-loop with requested timeline N is not a child of this server's history. The pg_stat_archiver view on the primary will show Last Failed WAL: 00000004.history with a non-zero failed_count while archived_count continues to grow, which is the diagnostic signature of this bug .
Switchover Deferral Mechanism#
archiver.Run() checks whether the current pod is still the CurrentPrimary in cluster status before archiving :
if cluster.Status.CurrentPrimary != podName → return errSwitchoverInProgress
This prevents the demoted pod from archiving WALs that the new primary should own, avoiding duplicate or conflicting archive entries. For replica clusters, the check is more permissive: pods that are neither CurrentPrimary nor TargetPrimary skip archiving silently with exit 0 .
The deferral is intentional — but the side effect on .history files (described above) is a bug.
Cross-Timeline WAL Segment Recovery Bug#
Issue: #10422
After a failover, PostgreSQL starts a new timeline at the precise LSN where the old timeline ended. If that LSN falls mid-WAL-segment, the resulting segment contains pages from both timelines:
- Offsets 0–
0xA0: pages withxlp_tli = N(old timeline) - Offsets
0xA0+: pages withxlp_tli = N+1(new timeline)
When a backup is taken immediately after the failover with its redo location inside this mixed segment, bootstrap recovery fails:
unexpected timeline ID 1 in WAL segment 000000020000000000000008, LSN 0/8000000, offset 0
FATAL: could not locate required checkpoint record
PostgreSQL reads from offset 0, finds the old timeline's header, and rejects the segment. No amount of retries resolves this — the archive content is structurally incompatible.
Scope: Any cluster with low write activity (checkpoint doesn't advance past the boundary segment before the next backup) and periodic switchovers (e.g., from rolling restarts with primaryUpdateMethod: restart) can have every backup affected.
Root causes#
-
pg_switch_wal()is not called after promotion.PromoteAndWait()issues aCHECKPOINT(required forpg_rewind) but does not force a WAL segment switch .pg_switch_wal()is only called inshipWalFile()during WAL archive bootstrapping verification , not in the promotion path. -
recovery_target_timelineis not set in bootstrap recovery config.getRestoreWalConfig()only emitsrecovery_target_action = promoteandrestore_command. For standby/replica configs it is explicitly set to"latest", but not for bootstrap recovery unless the user setsspec.bootstrap.recovery.recoveryTarget.targetTLI. -
History file restore guard (fixed in v1.29.2). Before PR #10818, the WAL restore command refused to fetch
.historyfiles when the recovering cluster'sstatus.timelineIDwas 0 (fileTimeline > clusterTimelinecheck). This blocked cross-timeline recovery even when the archive was healthy. Confirmed fixed in v1.29.2 .
Proposed fixes (not yet merged)#
- Call
pg_switch_wal()after promotion — prevents mixed-timeline segments from being created; the natural location is after the existingCHECKPOINTinPromoteAndWait(). - Reject backups whose redo WAL segment spans a timeline boundary — detect and warn/reject during backup validation.
Immediate workaround#
If a cluster suffers periodic failovers and backups consistently land in a cross-timeline segment, wait for a checkpoint to advance past the boundary segment (or force one with CHECKPOINT; SELECT pg_switch_wal(); on the primary) before taking a backup that you intend to restore from.
Related Issues#
| Issue | Summary |
|---|---|
| #10422 | Bootstrap recovery fails when backup redo WAL segment spans a timeline boundary |
| #11219 | .history file archival fails during failover and is never retried; replicas crash-loop |
| #10419 | Replica stuck in WAL-restore loop after timeline divergence |
| #4990 | Replicas crash-loop on timeline checkpoint failures (15+ affected users) |