primary_conninfo does not make a server a standby. standby.signal does that. What primary_conninfo does is tell a server that is already a standby where to ask for WAL, and outside that situation it is inert: on a primary it sits in pg_settings doing nothing, and on a server started with recovery.signal instead of standby.signal it is not even consulted; the server refuses to start (must specify "restore_command" when standby mode is not enabled), because streaming happens in standby mode and nowhere else.

The default is the empty string. The context is sighup since PostgreSQL 13. In 12, the first release where it was a GUC at all rather than a line in recovery.conf, it was postmaster, and before that every change meant a restart, all the way back to its arrival with streaming replication in 9.0. It is also one of the handful of parameters flagged superuser-only: SHOW primary_conninfo from an ordinary role gets permission denied to examine, and the row is simply missing from pg_settings unless you hold pg_read_all_settings. The reason is that it is a libpq connection string, and libpq connection strings tend to have passwords in them. We will get to that.

What the walreceiver hands to libpq

The server does not so much read this string as pass it along. When the startup process has run out of WAL in the archive and in pg_wal, and only then, it starts a walreceiver, and the walreceiver hands your string to libpq with three things appended: replication=true, dbname=replication, and fallback_application_name set to cluster_name (or to the literal walreceiver, if you have never set one). Each of those has a consequence.

dbname=replication comes after your string, so any dbname you wrote is overridden for the connection itself; the server ignores the database name on a physical replication connection anyway. Where the name matters is ~/.pgpass, which libpq searches using the database name it connected with. A .pgpass line with app or postgres in the database column will never match a walreceiver connection, however correct the rest of the line is; it needs replication there, or *. The symptom is a standby logging fe_sendauth: no password supplied every five seconds while everyone involved re-reads a .pgpass file that looks fine. (dbname has had one real use since 17: the slot-synchronization worker needs an ordinary database to connect to, and with sync_replication_slots otherwise set up and no dbname in the string it logs replication slot synchronization requires "dbname" to be specified in "primary_conninfo" until it gets one.)

fallback_application_name is why pg_stat_replication.application_name on the primary shows your cluster_name without your having asked, why the connection-failure message reads streaming replication receiver "s1" could not connect to the primary server, and what synchronous_standby_names matches against. Set cluster_name and leave application_name out of the string unless the two need to differ.

The “only then” matters as well. In standby mode the startup process works through its sources in a fixed order: the archive via restore_command, then pg_wal, then streaming, and when streaming fails it waits wal_retrieve_retry_interval (five seconds) and begins again at the archive. That loop is the five-second cadence of waiting for WAL to become available in a standby’s log, and of the FATAL from a walreceiver that cannot connect: one attempt per lap, forever, until something changes.

Anything you leave out of the string is filled in from the postmaster’s environment (PGHOST, PGUSER, PGPASSFILE and the rest), and anything libpq accepts is accepted here: sslmode, connect_timeout, tcp_user_timeout, and options. That last one passes -c settings into the walsender’s session on the upstream, and since wal_sender_timeout is user context, a standby can set the timeout on its own sender from its own side with options=''-c wal_sender_timeout=5000'' (the doubled quotes are because the whole thing is inside a single-quoted GUC value). The documentation’s own example does exactly this, with that value. I tried it with 5, five milliseconds, and the upstream killed the walsender on schedule.

Since 13, changing the string (or primary_slot_name, or wal_receiver_create_temp_slot) and reloading makes the startup process terminate the walreceiver and start another with the new value:

1LOG: parameter "primary_conninfo" changed to "host=127.0.0.1 port=5501 user=replicator ..."
2LOG: WAL receiver process shutdown requested
3FATAL: terminating walreceiver process due to administrator command
4LOG: started streaming WAL from primary at 0/3000000 on timeline 1

About ten milliseconds, first line to last. On 12 that was a restart, and so was every rewrite of this parameter by an HA tool at failover. The reverse is also available: set it to the empty string and reload, and the walreceiver is shut down and not replaced. The standby keeps polling pg_wal (and the archive, if there is a restore_command) every five seconds, pg_stat_wal_receiver goes empty, and the walsender on the primary is released. It is the quietest way I know to take a standby off the primary for a while without stopping it, and to put it back.

Where the password ends up

The string usually contains a password, and the superuser-only flag protects it in fewer places than the flag suggests. pg_stat_wal_receiver.conninfo is the one place that masks it (password=********); it also shows the string as libpq received it, appendages and all, which makes it the right place to look when the connection is doing something you did not write. SHOW and pg_settings show the password in the clear to anyone with pg_read_all_settings. And then there is the log. The first line of the transcript above is written at LOG level, with the complete value, every time a reload changes the parameter; LOG clears the default log_min_messages, so a stock configuration does this. With log_statement at ddl or above, the ALTER SYSTEM that set it is logged too. Logs are shipped to places pg_read_all_settings has never heard of.

postgresql.auto.conf is the fourth place, because that is where pg_basebackup -R writes the string, and what it writes is not what you typed. It is every libpq option that had a value, which on 18 is a wall of sslmode=prefer gssencmode=prefer target_session_attrs=any load_balance_hosts=disable and friends, plus, if the password came from PGPASSWORD, the -d string, or a -W prompt, password= in the clear, whether or not the server ever asked for one. If no password was supplied at all, you get passfile= instead, with the absolute path to the ~/.pgpass of whoever ran pg_basebackup, whether or not that file exists; which is fine until that is a different user from the one who will run the standby.

So: no password in the string. Put it in ~/.pgpass with replication in the database column, or point passfile at a file the standby’s user owns, or dispense with passwords and use cert authentication with sslcert and sslkey in the string. The documentation’s examples write password=foopass inline, three times on one page, and that is the kind of example that gets copied.

The string you did not write

pg_basebackup -R appends to postgresql.auto.conf; it does not replace it, and the file it appends to was copied from the source cluster. Take the backup from a standby and the result carries that standby’s primary_conninfo, with yours added after it (last wins, so that part is harmless), and that standby’s primary_slot_name, with nothing added after it unless you passed -S. The clone then starts up asking for a slot named after its sibling. If its upstream is the standby it was cloned from, the slot is not there, and the log says replication slot "s1_slot" does not exist every five seconds. If its upstream is the primary, the slot is there, and the clone spends every lap asking for it and being told replication slot "s1_slot" is active for PID, because a slot is held by one walsender at a time; the moment its sibling drops the connection, the clone takes the slot and the sibling gets the same message. Neither failure is subtle. Both have cost someone an afternoon.

Promotion leaves the same residue. pg_ctl promote removes standby.signal and leaves the configuration alone; the new primary keeps its primary_conninfo and primary_slot_name, inert, until the next pg_basebackup -R taken from it bakes them into a fresh standby. ALTER SYSTEM RESET both as part of promoting.

Following the primary

This is a libpq string, libpq has accepted a list of hosts since 10, and target_session_attrs says which of them will do. The walreceiver honours all of it:

1primary_conninfo = 'host=node1,node2,node3 port=5432 user=replicator
2 passfile=/var/lib/postgresql/.pgpass
3 target_session_attrs=read-write'

tries the hosts in order, declines the standbys (session is read-only in the log), and streams from whichever one is the primary. Stop the primary and the walreceiver fails every five seconds against the remaining standbys until one of them is promoted; then it connects there, fetches the timeline history, and carries on down the new timeline, because recovery_target_timeline has defaulted to latest since 12. On my test trio the whole event was one fetching timeline history file for timeline 2 and one restarted WAL streaming ... on timeline 2, with nobody editing anything. (14 added primary as a target_session_attrs value, but it needs the upstream to report in_hot_standby, which is also 14; read-write asks SHOW transaction_read_only, which a walsender has answered since 10, so it is the one that works against an older upstream.)

This is not failover. It decides nothing about who gets promoted, and a standby that had replayed past the point where the new primary diverged still needs pg_rewind. What it does is remove one configuration edit from the failover sequence, and if there is no HA tool making that edit for you, it is the difference between the surviving standbys following the new primary and the surviving standbys sitting there logging session is read-only until a human notices.

The whole string, then, wants to be short: host or hosts, port, user, a passfile or a certificate, and nothing you would mind seeing in a log file, because you will.