Skip to main content
RunBook Academy

PostgreSQLXIV · Replication, Slots and Read ReplicasReplication

Measuring lag properly: sent, written, flushed, replayed

Intermediate⏱ ~30 min🧪 Lab requiredpsql

What you'll learn

  • Distinguish the four LSNs and say which guarantee each represents
  • Choose between byte lag and time lag for a given question
  • Write a lag check that does not fire on a healthy idle cluster
  • Recognise lag that is caused by stopped replay rather than slow replay

Prerequisites

Practice

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

“Replication lag” is four different questions with four different answers, and using the wrong one produces monitoring that is confidently wrong.

The four LSNs

ColumnMeaningAnswers
sent_lsnThe walsender put it on the socketIs the primary keeping up?
write_lsnThe standby handed it to the OSIs the network keeping up?
flush_lsnThe standby fsynced itWould it survive a standby crash?
replay_lsnThe startup process applied itIs it visible to queries?

Each is a superset of the last, so sent >= write >= flush >= replay always holds.

The two that matter operationally are flush_lsn, which is the durability point and what synchronous replication waits for, and replay_lsn, which is the visibility point and what a read replica serves from.

Byte lag and time lag

Read-only / Safeboth units, side by side
$ SELECT application_name, write_lag, flush_lag, replay_lag,
     pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind
FROM pg_stat_replication;
 22:10:50  write_lag=00:00:00.000898  flush_lag=00:00:00.002532  replay_lag=00:00:00.004165  bytes_behind=43240
22:10:54  write_lag=00:00:00.000857  flush_lag=00:00:00.002503  replay_lag=00:00:00.002503  bytes_behind=9776
22:10:59  write_lag=00:00:00.000823  flush_lag=00:00:00.002483  replay_lag=00:00:00.002483  bytes_behind=26376

write < flush < replay every time, as it must.

Byte lag answers “how much data is at risk?” It is honest about volume and useless for judging staleness: 43 kB behind is nothing on a busy cluster and could be minutes of work on a quiet one.

Time lag answers “how stale is this replica?” It is what an application cares about. It is computed from timestamps in the WAL stream, so it is meaningful across clusters of different write rates.

Monitor both. Alert on time.

A lag check that works

-- Run on the PRIMARY. Returns one row per expected standby, plus a
-- count so that a missing standby is detectable rather than silent.
SELECT
  (SELECT count(*) FROM pg_stat_replication)          AS connected,
  application_name,
  state,
  coalesce(replay_lag, interval '0')                  AS replay_lag,
  pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)   AS bytes_behind,
  now() - reply_time                                  AS since_last_report
FROM pg_stat_replication;

Three alerting rules, and the first is the one that matters most:

  1. connected below the expected number. A standby that is gone produces no row at all.
  2. replay_lag above a threshold you chose from your RPO.
  3. since_last_report above roughly 2 × wal_receiver_status_interval — a standby that has stopped reporting but has not yet hit wal_sender_timeout shows stale LSNs that read as lag rather than absence.

From the standby’s side

The standby cannot see the primary’s current position, so it cannot compute true lag. What it can report:

-- how long since this standby applied anything from the WAL stream
SELECT
  pg_is_in_recovery()                            AS in_recovery,
  pg_last_wal_receive_lsn()                      AS received,
  pg_last_wal_replay_lsn()                       AS replayed,
  pg_last_xact_replay_timestamp()                AS last_replayed_txn,
  now() - pg_last_xact_replay_timestamp()        AS apparent_delay;

pg_last_xact_replay_timestamp() is the commit timestamp of the last transaction replayed. now() minus that is an apparent delay, and it has one important failure mode: on a primary with no write traffic it grows without bound while the standby is perfectly current. It measures “time since the last replayed commit”, which on an idle system is time since the last commit anywhere.

What to take from this

  • Four LSNs: sent, write, flush, replay. flush is durability; replay is visibility.
  • Byte lag for volume at risk; time lag for staleness. Alert on time.
  • NULL lag on an idle cluster is healthy. Do not alert on it.
  • Zero rows is the alert. max() over an empty set returns NULL and fires nothing.
  • pg_last_xact_replay_timestamp() grows on an idle primary even when the standby is current.
  • replay_lag tracking wall-clock exactly means replay is stopped, not slow.

Cross-course references

  • Observability for Production Sysadmins — Part XIII (Rates and counters) covers why a byte lag and a time lag answer different questions, and Part XX (Alert quality) covers why the time-based one fires every quiet weekend.
  • Linux for Production Sysadmins — Part XLII (Network Performance) covers proving the link rather than blaming it.

Quiz

Knowledge check · 6 questions

  1. Q1. A monitoring check alerts whenever replay_lag IS NULL in pg_stat_replication. It fires every night between 02:00 and 06:00. What is happening?

  2. Q2. replay_lag on a standby reads 5.99s at t+6s, 12.05s at t+12s and 30.00s at t+30s. What does that pattern indicate?

  3. Q3. Why does clock skew between primary and standby not affect the lag columns in pg_stat_replication?

  4. Q4. Which conditions should a replication monitoring check detect? Select all that apply.

  5. Q5. NULL values in the write_lag, flush_lag and replay_lag columns indicate that the primary has lost contact with the standby.

  6. Q6. What do flush_lag and replay_lag each tell you, and what does the difference between them measure?

Passing score: 75%. Answers are checked in this browser.