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 orwal_keep_sizeprevent. - Logical: the WAL is decoded, changes are reassembled per transaction in memory (
logical_decoding_work_mem, then spill topg_replslot/or stream), and at commit the output plugin (pgoutputfor 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:
Each walsender takes one of max_wal_senders slots (default 10, server restart to change). One more is refused:
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/13060000state 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_lsngap: network.flush→replaygap: 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.activeandwal_status. - Logical walsenders using a lot of CPU or disk under
pg_replslotare decoding big transactions; check that the subscription streams them (streaming = parallel, the default since PostgreSQL 18) instead ofoff.
For developers: inside the source
Where it lives
walsender.c: the process; commands are parsed byrepl_gram.y/repl_scanner.l.walsender_private.h: shared state.- Logical decoding:
src/backend/replication/logical(logical.c,decode.c,reorderbuffer.c,snapbuild.c) and the built-in output pluginsrc/backend/replication/pgoutput. Overview:README.
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 toGetFlushRecPtr()(orGetStandbyFlushRecPtr()when cascading), at mostMAX_SEND_SIZEperXLogData('w') message; reads viaWALReadFromBuffers()first, thenWALRead(), thenCheckXLogRemoved().XLogSendLogical():XLogReadRecord()→LogicalDecodingProcessRecord()→ reorder buffer → output plugin callbacks, which write throughWalSndPrepareWrite()/WalSndWriteData(). Waiting for new WAL happens inWalSndWaitForWal()(wait eventWalSenderWaitForWal).ProcessStandbyReplyMessage()updatesWalSnd, advances a physical slot viaPhysicalConfirmReceivedLocation()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
- TAP suites:
001_stream_rep.pl,019_replslot_limit.pl,src/test/subscription/t. - Talk the protocol yourself:
psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM";pg_recvlogicalwithcontrib/test_decodingshows decoded changes. - Writing an output plugin: export
_PG_output_plugin_init()fillingOutputPluginCallbacks(output_plugin.h).
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)