Replication lag is how far a standby trails the primary, and you monitor it on the primary through pg_stat_replication. The key realization is that lag has stages — there isn't one number — because WAL travels through several steps on the standby before a change becomes visible.
The view to know
SELECT client_addr, state,
sent_lsn, write_lsn, flush_lsn, replay_lsn,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
Each standby is one row. The LSN columns are positions in the WAL byte stream, and they map to the pipeline:
sent_lsn— WAL the primary has sent.write_lsn— WAL the standby has written to its OS.flush_lsn— WAL the standby has fsynced to disk (durable on the standby).replay_lsn— WAL the standby has actually applied — the point at which a query on the standby would see the change.
The gap between sent_lsn and replay_lsn is your real read-lag.
Measuring lag two ways
In bytes (how much WAL the standby is behind) — useful for spotting a standby falling off entirely:
SELECT client_addr,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes
FROM pg_stat_replication;
In time — the write_lag, flush_lag, replay_lag interval columns give the round-trip delay directly. On the standby itself, when the primary is idle and no new WAL arrives, byte-lag reads zero misleadingly; use the timestamp of the last replayed transaction:
-- run on the standby
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;
What causes lag and what to do
The usual culprits: a write-heavy primary outpacing the standby, slow standby disk, network saturation, or a long-running query on the standby blocking WAL replay (conflict with hot_standby_feedback). Alert on replay_lag (that's what affects readers) and on byte-lag growth (a standby silently falling behind risks needing a full rebuild).