# How do I handle read-after-write when replication lag hides a user's own just-written data?

> Route a user's reads to the primary for a few seconds after their write, and reach for LSN fencing only if that window isn't precise enough.

- Asked: 2026-11-01
- Answered: 2026-11-03
- Asked by: Nehir
- Tags: PostgreSQL, Transactions
- Source: https://muhammetsafak.com/just-ask/read-after-write-lag-hides-just-written-data/
- Language: en-US
- Author: Muhammet Şafak

---
**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?


## 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](https://muhammetsafak.com/just-ask/when-to-apply-cqrs-and-syncing-read-and-write-models/). 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.
   ```php
   // 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.
   ```sql
   -- 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.

## Related Reading

- [Should I use a GIN index or a generated column to filter on fields inside a JSONB column?](https://muhammetsafak.com/just-ask/gin-index-generated-column-filter-inside-jsonb-column/) — Just Ask
- [When should I switch to a covering index with INCLUDE to get an index-only scan?](https://muhammetsafak.com/just-ask/switch-covering-index-include-get-index-only-scan/) — Just Ask
- [Should I use a partial index on a queue table where I only ever scan 'pending' rows?](https://muhammetsafak.com/just-ask/should-i-use-a-partial-index-on-a-queue-table-where/) — Just Ask
