DB Internals AtlasPostgreSQL 18.6 Home About

Background processes

WAL receiver

On a streaming standby, the walreceiver connects to the primary, receives WAL, writes and flushes it to the local pg_wal, and reports its progress back.

In plain words

A standby server keeps itself up to date by replaying the primary's WAL. The walreceiver is the process that fetches it: it logs in to the primary like a client (using primary_conninfo), receives the stream of WAL, and saves it to the standby's own pg_wal directory.

It does not apply the changes itself. That is the job of the startup process, which the walreceiver wakes up whenever new WAL is safely on disk. In the other direction it regularly tells the primary how far it got, which is what the primary shows in pg_stat_replication.

If the connection breaks, it simply exits; the startup process starts a new one a few seconds later. Toggle hot_standby_feedback to see what extra information it sends.

For DBAs and operators

Who starts it, and when

Only the startup process asks for a walreceiver: when the standby (with standby.signal) has replayed all WAL it can find in pg_wal and the archive, and primary_conninfo is set. The postmaster then forks it. It connects with replication=true, runs IDENTIFY_SYSTEM (the system identifier must match), fetches timeline history files if needed, and sends START_REPLICATION from the start of the WAL segment it needs, using primary_slot_name if set (or a temporary slot with wal_receiver_create_temp_slot).

LOG: started streaming WAL from primary at 0/4000000 on timeline 1

The loop

  1. Receive XLogData and write it into the current segment file in pg_wal (written_lsn).
  2. When the socket is drained, fsync it (flushed_lsn) and wake the startup process.
  3. Send a status reply (write/flush/apply positions) after each flush and at least every wal_receiver_status_interval (10s). If hot_standby_feedback = on, also send the oldest xmin of queries on the standby, which appears as backend_xmin in the primary's pg_stat_replication (and holds back VACUUM there).
  4. Answer keepalives. If nothing arrives for wal_receiver_timeout (60s), give up.

How to see it

SELECT pid, status, receive_start_lsn, written_lsn, flushed_lsn,
       latest_end_lsn, last_msg_receipt_time, slot_name,
       sender_host, sender_port
  FROM pg_stat_wal_receiver;

SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn(),
       now() - pg_last_xact_replay_timestamp() AS replay_delay;

backend_type is walreceiver; ps shows postgres: walreceiver streaming 0/13000000. pg_stat_wal_receiver returns no row when no walreceiver is running: that is your first clue that streaming is down.

If it dies

Errors make it exit with FATAL; the startup process falls back to the archive and pg_wal, and asks for a new walreceiver after wal_retrieve_retry_interval (5s). Real lines from a test standby:

FATAL: terminating walreceiver process due to administrator commandLOG: invalid record length at 0/55908D78: expected at least 24, got 0LOG: started streaming WAL from primary at 0/55000000 on timeline 1

The "invalid record length" line is normal: the startup process reached the end of the WAL available locally. Other typical messages:

FATAL: terminating walreceiver due to timeoutFATAL: streaming replication receiver "walreceiver" could not connect to the primary server: connection to server at "10.0.0.1", port 5432 failed: Connection refusedFATAL: could not receive data from WAL stream: ERROR: requested WAL segment 000000010000000000000012 has already been removed

The last one means the primary recycled WAL the standby still needed: use a replication slot or wal_keep_size, or let restore_command fetch it from the archive. A crash by signal (SIGKILL) restarts the whole standby like any child crash.

Support notes

  • primary_conninfo and primary_slot_name are reloadable since PostgreSQL 13: a reload restarts the walreceiver.
  • written_lsn ahead of replay is normal; a growing gap is a slow or blocked startup process (see recovery conflicts), not the network.
  • hot_standby_feedback = on prevents most snapshot conflicts but can bloat the primary if standby queries run for hours.

For developers: inside the source

Where it lives

  • walreceiver.c: the process (WalReceiverMain()).
  • walreceiverfuncs.c: functions the startup process uses to control it (RequestXLogStreaming(), ShutdownWalRcv(), WalRcvStreaming(), GetWalRcvFlushRecPtr()).
  • libpqwalreceiver.c: the libpq part, loaded at run time with load_file("libpqwalreceiver") so the backend binary does not link libpq. It fills the WalReceiverFunctionsType table used through macros such as walrcv_connect(), walrcv_startstreaming(), walrcv_receive(), walrcv_send(); logical replication apply workers use the same table.
  • walreceiver.h: shared state and the function table.

Key data structures

WalRcvData (one in shared memory, WalRcv) holds walRcvState (WALRCV_STOPPED, STARTING, STREAMING, WAITING, RESTARTING, STOPPING), the requested receiveStart/receiveStartTLI, flushedUpto, latestWalEnd, conninfo, slotname and sender host/port, protected by a mutex spinlock. writtenUpto is a pg_atomic_uint64 updated without the lock. pg_stat_get_wal_receiver() reads it for pg_stat_wal_receiver.

Call flow

  1. The startup process, in WaitForWALToBecomeAvailable() (xlogrecovery.c), calls RequestXLogStreaming(), which sets WALRCV_STARTING and sends PMSIGNAL_START_WALRECEIVER; the postmaster forks the process.
  2. WalReceiverMain(): walrcv_connect(), walrcv_identify_system(), WalRcvFetchTimeLineHistoryFiles(), walrcv_startstreaming().
  3. Inner loop: walrcv_receive() → XLogWalRcvProcessMsg() → XLogWalRcvWrite() for 'w' messages, ProcessWalSndrMessage() for keepalives. When the socket is drained, XLogWalRcvFlush() does issue_xlog_fsync(), advances flushedUpto, calls WakeupRecovery() (and WalSndWakeup() for cascading standbys), then XLogWalRcvSendReply() and XLogWalRcvSendHSFeedback().
  4. Sleeping uses WaitLatchOrSocket() (wait event WalReceiverMain) with a timeout from WalRcvComputeNextWakeup(), which keeps one deadline per WalRcvWakeupReason: WALRCV_WAKEUP_TERMINATE (wal_receiver_timeout), PING, REPLY, HSFEEDBACK.
  5. When the primary ends streaming (e.g. a timeline switch), the process does not exit: WalRcvWaitForStartPosition() parks it in WALRCV_WAITING until the startup process asks for a new start point.

Experimenting

Parameters that shape this

primary_conninfo primary_slot_name wal_receiver_status_interval hot_standby_feedback wal_receiver_timeout wal_retrieve_retry_interval wal_receiver_create_temp_slot recovery_min_apply_delay

Related topics

WAL sender Startup process Streaming replication Hot standby and recovery conflicts Replication slots Synchronous replication Archiving and point-in-time recovery