How do I handle read-after-write when replication lag hides a user's own just-written data?
Question
I run PostgreSQL with a primary and a read replica, and I route read queries to the replica to scale reads. But right after a user saves their profile, the next page reads from the replica and shows stale data. Because the user can't see their own just-written data, they keep re-saving, wondering "did it not save?" How do I solve this read-after-write consistency on the application side?
Answer
Short answer
This is the classic read-your-writes problem.
Context
Don’t try to make replication instant — route the narrow set of reads that must see fresh data to the primary, and leave the rest on the replica. Do it surgically, not globally.
Why
- Accept the tradeoff first. Async replication is lagged by design; that’s the price of read scaling. You won’t eliminate the lag — you only route around it for the narrow reads where staleness is user-visible.
What to do
- Simplest robust fix: a sticky-primary window after a write. After a user writes, pin their reads to the primary for a short window (say, a few seconds). Laravel gives you this out of the box:
'sticky' => truein the DB config sends the rest of a request that performed a write to the writer.// config/database.php — reads go to the replica, writes to the primary, // and any request that writes sticks to the primary for its remaining reads. 'pgsql' => [ 'read' => ['host' => ['replica.internal']], 'write' => ['host' => ['primary.internal']], 'sticky' => true, // ... ], - The sticky flag only covers the same request. Your bug is a next page read — a new request. So you need a longer-lived signal: store a “last write at” timestamp per user (session/Redis) and route reads to the primary while
now - last_write < lag_budget. - A more precise approach: LSN fencing. Capture the write’s WAL LSN (
pg_current_wal_lsn()), carry it forward (cookie/token), and on read either wait until the replica has replayed past it (pg_last_wal_replay_lsn()) or fall back to the primary. This gives correctness without sending everyone to the primary — more plumbing, but exact.-- On write, capture the position: SELECT pg_current_wal_lsn(); -- On read from the replica, verify it has caught up: SELECT pg_last_wal_replay_lsn() >= '0/16B62C0'::pg_lsn; - Scope it to the user’s own data. Read-your-writes only matters for data the writer sees. Other users tolerating a couple of seconds of staleness is usually fine. Don’t send the whole app to the primary and lose your read scaling.
- Monitor the lag. Alert on
pg_last_xact_replay_timestamp()/ replication lag. If lag regularly exceeds your budget, no application trick will save you — fix the replica (I/O, long transactions on the primary, network).
Bottom line: personally I’d start with a per-user “read from primary for N seconds after a write” rule — it’s a few lines, covers the profile-save case exactly, and keeps the replicas doing the heavy lifting for everyone else. I’d only build LSN fencing if a few seconds of primary routing wasn’t precise enough, or the write volume made it too costly. A global “just read the primary” defeats the reason you added a replica in the first place.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.