Skip to content
Muhammet Şafak
tr
Asked by: Nehir Answered:

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

  1. 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

  1. 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' => true in 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,
        // ...
    ],
  2. 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.
  3. 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;
  4. 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.
  5. 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.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind