After a successful save, some users still see the old data for a while. What could cause this?

Entity Framework & EF Core Scenario Advanced 3–5 Yrs5+ Yrs Scenario 2 min read

Quick Answer

Something between the write and that user's read is serving an older version: an application or output cache that wasn't invalidated, a long-lived DbContext returning tracked (pre-update) entities, HTTP/CDN caching without proper Cache-Control/ETag, a read replica that lags the primary, or a second app instance with its own in-memory cache. Identify which layer by checking whether a forced fresh query on the server returns new or old data.

Something between the write and the read is caching an old copy, and the job is to find which layer. I'd start at the database — if we're using read replicas, the read might be hitting one that's a few seconds behind the primary, so read-after-write should go to the primary. Then the ORM — a DbContext that isn't scoped per request will hand back the entity it tracked before the update, so scoped lifetime plus AsNoTracking for reads. Then application caching — any write has to invalidate the cache keys for what it changed, and if we're running multiple instances behind a load balancer, that cache has to be shared or you get one instance serving stale data. And finally HTTP caching — a GET without Cache-Control or an ETag can get cached by a proxy or the browser. To narrow it down I'd call the API directly on the server with a cache-buster: if that's fresh, it's a caching layer; if it's stale there too, it's the replica or the context.

Work outward from the database:

  • Database — if you use read replicas, reads may hit a replica that's seconds behind the primary. Route read-after-write to the primary, or accept the window.
  • ORM — a DbContext that lives too long (not scoped-per-request, captured in a singleton) hands back the entity it tracked earlier. Scoped lifetime + AsNoTracking for reads avoids it.
  • Application cache — an IMemoryCache/Redis entry keyed by the resource that isn't evicted on write. Every write path must invalidate (or version) the relevant keys.
  • Multiple instances — behind a load balancer, instance A wrote and evicted its local cache; instance B still has the stale copy. Use a shared/distributed cache or a pub/sub invalidation, not per-instance memory.
  • HTTP / CDN / browser — a GET response cached by a proxy or the browser because it lacked Cache-Control: no-cache or an ETag. Add validation headers; bust query params for critical reads.

Diagnosis: hit the API directly on the server with a cache-busting request. Fresh data there → a caching/CDN layer is the culprit. Stale there too → replica lag or the ORM.