DB Internals AtlasPostgreSQL 18.6 Home About

Background processes

WAL sender

For every replication connection (a standby, pg_basebackup, pg_receivewal or a logical subscriber) the primary runs one walsender process that reads WAL and streams it to the other side.

In plain words

A WAL sender is the primary's side of a replication connection. When a standby server (or a backup tool, or a logical replication subscriber) connects in replication mode, the postmaster starts a walsender for it instead of a normal backend. One connection, one walsender.

Its job is a loop: wait until new WAL has been safely written to disk, read it, send it, and listen for the other side's replies saying how far it has written, flushed and replayed. Those replies are what you see in pg_stat_replication.

A physical walsender ships the WAL bytes unchanged. A logical walsender decodes the WAL into row changes (INSERT, UPDATE, DELETE) for the tables in a publication. Pick the kind with the selector and step through.

For DBAs and operators

Who starts it

A client connects with replication=true (physical) or replication=database (logical). The postmaster forks a process as usual; after authentication against a replication entry in pg_hba.conf (physical) or a normal database entry (logical), it becomes a walsender and accepts replication commands (IDENTIFY_SYSTEM, START_REPLICATION, CREATE_REPLICATION_SLOT, BASE_BACKUP …). Set log_replication_commands = on to log them.

The loop

  • Physical: send everything up to the WAL flush position (never unflushed WAL). Since PostgreSQL 17 it first tries to copy it straight from WAL buffers, otherwise it reads the segment from pg_wal (usually from the OS cache). If the segment was already removed: ERROR: requested WAL segment 000000010000000000000012 has already been removed That is what replication slots or wal_keep_size prevent.
  • Logical: the WAL is decoded, changes are reassembled per transaction in memory (logical_decoding_work_mem, then spill to pg_replslot/ or stream), and at commit the output plugin (pgoutput for built-in replication) turns them into messages filtered by the publication. Always uses a slot.
  • Between sends it waits on its latch; WAL flushes wake it.

Replies, keepalives, timeout

The receiver reports write, flush and apply positions (every wal_receiver_status_interval and after writes). With no traffic the walsender sends keepalives, asking for a reply once half of wal_sender_timeout (60s) has passed. If nothing comes back in time:

LOG: terminating walsender process due to replication timeout

Each walsender takes one of max_wal_senders slots (default 10, server restart to change). One more is refused:

FATAL: number of requested standby connections exceeds "max_wal_senders" (currently 10)

How to see it

SELECT pid, application_name, client_addr, state, sync_state,
       sent_lsn, write_lsn, flush_lsn, replay_lsn,
       write_lag, flush_lag, replay_lag,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_bytes_behind
  FROM pg_stat_replication;

backend_type is walsender in pg_stat_activity, where query shows the replication command (e.g. START_REPLICATION 0/4000000 TIMELINE 1). In ps:

postgres: walsender postgres 10.0.0.2(51234) streaming 0/13060000

state goes startup → catchup → streaming (or backup during pg_basebackup). Note that a lag column is NULL when the standby is fully caught up and idle.

If it dies

A walsender exiting with an error (timeout, network drop, pg_terminate_backend()) affects only that connection; the standby's walreceiver reconnects. A crash (signal such as SIGKILL or a segfault) triggers the normal crash-restart of the whole primary, like any backend crash.

Support notes

  • Big sent_lsn → write_lsn gap: network. flush → replay gap: the standby's startup process (conflicts, slow I/O, recovery_min_apply_delay).
  • An inactive slot does not need a walsender but still retains WAL: check pg_replication_slots.active and wal_status.
  • Logical walsenders using a lot of CPU or disk under pg_replslot are decoding big transactions; check that the subscription streams them (streaming = parallel, the default since PostgreSQL 18) instead of off.

For developers: inside the source

Where it lives

Key data structures

WalSndCtlData holds the sync-rep queues (under SyncRepLock), condition variables wal_flush_cv/wal_replay_cv used to wake walsenders, and an array of WalSnd, one per max_wal_senders slot. A WalSnd has pid, state (WalSndState: STARTUP, BACKUP, CATCHUP, STREAMING, STOPPING), sentPtr, the standby's write/flush/apply positions, measured lags and sync_standby_priority, protected by its own mutex spinlock. pg_stat_get_wal_senders() reads this array to build pg_stat_replication.

Call flow

A replication connection is a normal backend with am_walsender set: PostgresMain() hands each query string to exec_replication_command(). START_REPLICATION goes to StartReplication() (physical) or StartLogicalReplication(), both ending in WalSndLoop() with a send callback:

WalSndLoop(send_data):
  ProcessRepliesIfAny();        /* standby reply / hot-standby feedback */
  send_data();                  /* XLogSendPhysical or XLogSendLogical */
  WalSndCheckTimeOut(); WalSndKeepaliveIfNecessary();
  WalSndWait(..., WAIT_EVENT_WAL_SENDER_MAIN);
  • XLogSendPhysical(): sends up to GetFlushRecPtr() (or GetStandbyFlushRecPtr() when cascading), at most MAX_SEND_SIZE per XLogData ('w') message; reads via WALReadFromBuffers() first, then WALRead(), then CheckXLogRemoved().
  • XLogSendLogical(): XLogReadRecord() → LogicalDecodingProcessRecord() → reorder buffer → output plugin callbacks, which write through WalSndPrepareWrite()/WalSndWriteData(). Waiting for new WAL happens in WalSndWaitForWal() (wait event WalSenderWaitForWal).
  • ProcessStandbyReplyMessage() updates WalSnd, advances a physical slot via PhysicalConfirmReceivedLocation() and releases sync-rep waiters; ProcessStandbyHSFeedbackMessage() sets the xmin. LagTrackerWrite()/LagTrackerRead() compute the lag columns.
  • Backends wake walsenders after a WAL flush with WalSndWakeupProcessRequests() → WalSndWakeup().

Experimenting

Parameters that shape this

max_wal_senders wal_sender_timeout wal_level max_replication_slots wal_keep_size max_slot_wal_keep_size logical_decoding_work_mem log_replication_commands synchronized_standby_slots

Related topics

WAL receiver Streaming replication Replication slots Logical replication Logical replication workers Synchronous replication Postmaster Write-ahead log (WAL)