Dele

Why Your App Shows Stale Data Right After a Write: Understanding Read Replica Lag

September 25, 2026

Read replicas help databases scale, but they introduce a subtle bug where users don't see their own recent changes. Here's why that happens and how teams actually fix it.

Ayodele Miracle JohnAyodele Miracle JohnSOFTWARE ENGINEER
Why Your App Shows Stale Data Right After a Write: Understanding Read Replica Lag

The Bug That Isn't a Bug

A user updates their profile, hits save, gets redirected to their profile page, and sees their old name still sitting there. They refresh. Now it's correct. No error was thrown, nothing crashed, and the data was never actually lost. This is one of the most common "phantom bugs" reported against systems that use database read replicas, and it isn't really a bug at all. It's the expected behavior of an architecture that most engineers add for scaling reasons without fully accounting for its consistency tradeoffs.

How Replication Actually Works

Most relational databases (Postgres, MySQL, and their managed cloud equivalents) scale read traffic by running one primary node that accepts writes and one or more replica nodes that only serve reads. The primary streams a continuous log of changes, called a write-ahead log (WAL) in Postgres or a binlog in MySQL, to each replica. The replica applies those changes in order and stays a live copy of the primary.

The important detail is that this streaming happens over the network, asynchronously by default. The primary doesn't wait for a replica to confirm it received and applied a change before telling the client the write succeeded. That handoff is fast, usually single-digit milliseconds, but it is never zero. During that gap, the replica is technically behind the primary. That gap is replication lag.

Why the Lag Happens

Lag isn't a sign of misconfiguration. It's inherent to asynchronous replication, and it grows under a few predictable conditions: network latency between the primary and replica (worse across availability zones or regions), a replica that's under heavy read load and can't apply incoming changes fast enough, long-running transactions or large batch writes that take time to replicate and replay, and vacuum or maintenance operations competing for I/O on the replica.

Under normal conditions, lag might sit around 10 to 50 milliseconds. Under load or during a large migration, it can stretch to seconds, and in unhealthy setups, minutes. The application code usually has no idea any of this is happening. It just issues a read query to whatever connection pool routes to the replica, and gets back whatever the replica currently has.

The Classic Symptom: Read Your Own Write

The failure mode almost every team eventually hits is called read-after-write inconsistency, sometimes shortened to "read your own writes." The sequence is: a client writes to the primary, the write is acknowledged, the client immediately issues a read (often as part of returning a confirmation page or an API response), that read gets routed to a replica, and the replica hasn't caught up yet.

This is especially visible in write-then-redirect flows: submitting a form and being sent to a page that reloads the data, submitting a comment and not seeing it appear in the same request, or updating a setting and having the UI briefly flash the old value. None of this shows up in a typical staging environment, because the lag there is usually near zero and load is low. It tends to surface in production exactly when you have enough traffic to need read replicas in the first place, which makes it a frustrating one to reproduce and debug.

Fixing It: Four Practical Patterns

Teams generally reach for one of a handful of approaches, and most production systems end up combining two or three of them.

Route reads to the primary right after a write. The simplest fix: for a short window after a user's own write (often just for that request, or for a few seconds via a session flag), force reads for that user back to the primary instead of a replica. This is often called "read your own writes" routing and is the most common fix because it requires no schema changes, only routing logic in the application or ORM layer.

Use monotonic or "sticky" read consistency. Instead of routing every read to a random replica, pin a user's session to a single replica for a request lifecycle, or track the log sequence number (LSN) their last write produced and only route their next read to a replica that has caught up past that LSN. Postgres exposes this via functions like pg_last_wal_replay_lsn(), and some managed services (Aurora, for instance) expose similar mechanisms natively.

Return the written data directly instead of re-reading it. If a write handler already has the row it just inserted or updated, the simplest fix is often to hand that data straight back in the response rather than issuing a fresh SELECT against a replica at all. This sidesteps the consistency problem entirely for the common case of "show the user what they just saved."

Accept eventual consistency where it's genuinely fine. Not every read needs strong consistency. A public dashboard, an analytics view, or a activity feed that's a few hundred milliseconds behind is usually an acceptable tradeoff for the read scalability replicas provide. The key is being deliberate about which reads need freshness guarantees and which don't, rather than treating all reads the same.

Monitoring Lag Before It Becomes a Support Ticket

Because this failure is intermittent and load-dependent, it's worth tracking replication lag as a first-class metric rather than discovering it through user complaints. Postgres exposes lag directly through the pg_stat_replication view on the primary, and managed services like AWS RDS and Aurora surface a ReplicaLag CloudWatch metric out of the box. Alerting on lag crossing a threshold (say, 500ms sustained) gives you a warning before it turns into a wave of "my changes didn't save" tickets.

The underlying lesson is that read replicas trade strong consistency for read throughput, and that tradeoff needs to be a conscious decision made at the point where reads happen, not an assumption baked in silently by the connection pool.

/More Articles