Replica removes primary_conninfo and stops streaming after a brief DCS blip, with no failover and a confirmed-alive primary
What happened?
A brief Kubernetes API (DCS) outage caused the replica to remove primary_conninfo and primary_slot_name, kill the walreceiver, and fall back to archive recovery. It never reconnected. It stayed that way for 4h23m, reporting itself as a healthy replica following the leader, until we restarted the pod.
The distinguishing detail from #3686 and #3675: there was no failover. The primary was never demoted, no re-election occurred, and the timeline never changed. Throughout the window the primary logged only no action. I am (postgres-0), the leader with the lock.
The replica also had contact with the primary at the moment it disconnected. 158ms before stripping its config, its own health check of the primary returned:
{"role": "primary", "timeline": 14,
"replication": [{"application_name": "postgres-1", "state": "streaming", "sync_state": "async"}]}and it logged Primary (postgres-0) is still alive twice in that same second.
Impact: reads are served by the replica, so the application returned data minutes to an hour stale. Rows written to the primary and read back immediately were not found. Access tokens written to the primary were rejected as invalid for 30 minutes, until the archived WAL containing them reached the replica.
How can we reproduce it (as minimally and precisely as possible)?
We have not reproduced it deliberately. Observed conditions:
- Two-node cluster (one primary, one replica), Kubernetes DCS,
failsafe_mode: true. - Cluster healthy and streaming normally.
- The replica's Patroni loses access to the Kubernetes API for a few seconds (
[Errno 111] Connection refused, thenK8sConnectionFailed('No more API server nodes in the cluster')). - The primary is unaffected and keeps its leader lock throughout.
- Within ~13 seconds the replica strips
primary_conninfo/primary_slot_nameand terminates the walreceiver. - The DCS becomes reachable again seconds later. The replica does not restore the configuration.
What did you expect to happen?
Either:
- The replica keeps streaming. It had just confirmed the primary was alive and holding the leader lock, and no failover was in progress.
- Or, failing that, the HA loop restores
primary_conninfoand resumes streaming once the DCS is reachable again, rather than remaining in archive recovery indefinitely while reporting itself healthy.
Patroni/PostgreSQL/DCS version
Patroni 4.0.5
PostgreSQL 15.13
DCS: Kubernetes (use_endpoints: true)Patroni configuration file
scope: attentiontag-postgres
name: ${POD_NAME}
namespace: ${POD_NAMESPACE}
restapi:
listen: 0.0.0.0:8008
connect_address: ${POD_IP}:8008
kubernetes:
use_endpoints: true
labels:
app: postgres
role_label: role
bootstrap:
dcs:
failsafe_mode: true
slots:
postgres_0:
type: physical
postgres_1:
type: physical
postgresql:
parameters:
max_locks_per_transaction: 512
initdb:
- encoding: UTF8
pg_hba:
- local all all trust
- host replication replicator 0.0.0.0/0 scram-sha-256
- host all all 0.0.0.0/0 scram-sha-256
post_bootstrap: /scripts/bootstrap.sh
postgresql:
listen: 0.0.0.0:5432
connect_address: ${POD_IP}:5432
data_dir: /home/postgres/pgdata/data
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: ${PATRONI_REPLICATION_PASSWORD}
superuser:
username: ${PATRONI_SUPERUSER_USERNAME}
password: ${PATRONI_SUPERUSER_PASSWORD}
recovery_conf:
restore_command: python3 /scripts/wal_archive.py restore %f %p
parameters:
shared_preload_libraries: timescaledb
wal_level: replica
archive_mode: "on"
archive_command: python3 /scripts/wal_archive.py archive %p %f
hot_standby_feedback: "on"
max_replication_slots: 5
max_connections: 200
wal_keep_size: 128MB
shared_buffers: 2GB
effective_cache_size: 6GB
maintenance_work_mem: 256MB
work_mem: 10485kB
wal_buffers: 16MB
min_wal_size: 512MB
default_statistics_target: "100"
random_page_cost: "1.1"
checkpoint_completion_target: "0.9"
max_locks_per_transaction: "512"
autovacuum_max_workers: "10"
autovacuum_naptime: 10
jit: "off"
effective_io_concurrency: "256"
idle_in_transaction_session_timeout: "300s"patronictl show-config
patronictl could not be run from a one-shot kubectl exec (its Kubernetes object cache does not warm up in time and it exits with KubernetesError: Kubernetes API is not responding properly). The equivalent DCS configuration, from the /config REST endpoint:
failsafe_mode: true
postgresql:
parameters:
max_locks_per_transaction: 512
slots:
postgres_0:
type: physical
postgres_1:
type: physicalPatroni log files
Replica. DCS lost, then teardown 13 seconds later:
20:44:47,170 ERROR: Request to server failed: MaxRetryError(... [Errno 111] Connection refused)
20:44:48,173 ERROR: ObjectCache.run K8sConnectionFailed('No more API server nodes in the cluster')
20:44:48,175 ERROR: ObjectCache.run K8sConnectionFailed('No more API server nodes in the cluster')
20:45:00,529 WARNING: Primary (postgres-0) is still alive
20:45:00,616 INFO: following a different leader because i am not the healthiest node
20:45:00,627 WARNING: Primary (postgres-0) is still alive
20:45:00,666 INFO: following a different leader because i am not the healthiest nodeReplica, for the following 4h23m, every 10 seconds:
00:52:20,638 INFO: no action. I am (postgres-1), a secondary, and following a leader (postgres-0)Primary, across the same window — no demotion, no re-election:
20:44:40,699 INFO: no action. I am (postgres-0), the leader with the lock
20:45:00,977 INFO: no action. I am (postgres-0), the leader with the lock
20:49:50,723 INFO: no action. I am (postgres-0), the leader with the lockPostgreSQL log files
Replica, teardown:
2026-09-07 20:45:00.665 UTC [25] LOG: received SIGHUP, reloading configuration files
2026-09-07 20:45:00.670 UTC [25] LOG: parameter "primary_conninfo" removed from configuration file, reset to default
2026-09-07 20:45:00.670 UTC [25] LOG: parameter "primary_slot_name" removed from configuration file, reset to default
2026-09-07 20:45:00.684 UTC [29] LOG: WAL receiver process shutdown requested
2026-09-07 20:45:00.687 UTC [73] FATAL: terminating walreceiver process due to administrator command
2026-09-07 20:45:04.565 UTC [29] LOG: invalid resource manager ID 101 at 8/E96B1EE8
2026-09-07 20:45:05.398 UTC [29] LOG: waiting for WAL to become available at 8/E9002000Replica, afterwards — only hourly archive restores, never streaming again:
2026-09-07 21:04:19.124 UTC [29] LOG: restored log file "0000000E00000008000000E9" from archive
2026-09-07 22:01:35.761 UTC [29] LOG: restored log file "0000000E00000008000000EA" from archive
2026-09-07 23:00:42.710 UTC [29] LOG: restored log file "0000000E00000008000000EB" from archive
2026-09-07 23:48:39.231 UTC [29] LOG: restored log file "0000000E00000008000000EC" from archive
2026-09-08 00:47:06.045 UTC [29] LOG: restored log file "0000000E00000008000000ED" from archive
2026-09-08 00:52:22.044 UTC [29] LOG: waiting for WAL to become available at 8/EE000739Have you tried to use GitHub issue search?
- Yes
Anything else we need to know?
SHOW primary_conninfo on the replica returned empty, and pg_stat_replication on the primary returned no rows, for the full duration.
/cluster did expose the degraded state, but role still read as a normal replica, so anything keying off Patroni's own status saw a healthy cluster:
{"name": "postgres-1", "role": "replica", "state": "in archive recovery", "lag": 2062944}Restarting the replica pod resolved it immediately: configuration was rebuilt from the DCS, streaming resumed at 0.005s replay lag, and no rebuild was required.
Related: #3686 and #3675 report the same non-recovery after follow(node_to_follow=None) strips the recovery configuration. Both involve a failover and a new timeline. This case shows the same outcome with no failover, no re-election, no timeline change, and a primary that never lost its leader lock.
Source: patroni/patroni