Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Data Access in .NET

    Cache Correctness in ASP.NET Core: back to the course

    Module 1 · Safe reads and concurrent cache refreshes · Lesson 2 of 3

    Absolute and Sliding Expiration with Invalidation Races

    Learning outcome

    Choose between absolute expiration and sliding expiration for a cache entry, and reason about the stale-data window that appears when reads and writes overlap. You will also learn why a source write, cache removal, and an in-flight fill are separate operations, especially when the cache is used as an optimization rather than as the source of truth.

    Intuition

    Expiration is a policy for how long a cached value may survive before it is treated as invalid. Absolute expiration says, “stop serving this entry after its deadline.” Sliding expiration says, “keep this item alive while it is being used, but let it die if nobody touches it for a while.”

    That distinction matters because the cache is almost never the authoritative store. In a product catalog, the database still owns the truth about product name, price, and publish state. The cache only reduces read latency. If a product is renamed, the write path must update the source of truth first and then invalidate or refresh the cache. Even then, a concurrent reader may observe a stale entry for a brief moment.

    A simple mental model is this:

    Text
    read path -> cache hit? use value -> cache miss? load from source -> store in cache
    write path -> persist change -> invalidate or replace cache entry

    The problem is that these steps are not atomic across threads, and they are definitely not atomic across multiple app servers or a distributed cache.

    Deep dive

    Absolute expiration sets an upper bound on an individual entry's cache lifetime. If a catalog entry must expire within five minutes of being inserted, frequent reads do not extend that deadline. This does not by itself guarantee that the underlying data is at most five minutes stale: a delayed fill, stale replica or older snapshot can insert already stale data. Business freshness requirements may also need version checks or reads from an authoritative store. Sliding expiration is best when you want frequently accessed data to stay warm and cold data to evaporate. It is a throughput optimization, not a freshness guarantee.

    A useful rule of thumb:

    • Use absolute expiration when you need a predictable maximum lifetime.
    • Use sliding expiration when access frequency is a good proxy for usefulness.
    • Often use both together to get a practical cap plus activity-based renewal.

    In practice, the combination is common: sliding keeps hot items alive, while absolute prevents them from surviving forever.

    Why invalidation races happen

    Suppose a product price changes from 100 to 120.

    1. The cache contains the old price.
    1. Request B commits the new price to the database.
    1. Before B deletes the cache key, request C reads the old cached price.
    1. B deletes the key, and a later request D misses and reloads the new price.

    This is an invalidation race: the cache entry is logically stale after the write, but some readers can still see it in the gap between persistence and invalidation, or they can rebuild it from an older snapshot before the new value is fully visible.

    The key idea is that cache invalidation reduces the stale window; it does not magically eliminate it. A cache entry is only as fresh as the coordination around it.

    A successful deletion on the same strongly consistent cache is different from a pending invalidation: a later read cannot hit the deleted entry unless another fill repopulates it. A stale result after a completed deletion needs an explicit cause, such as an in-flight fill that read an older database snapshot before the write, or a separate stale cache replica. Keep these timelines distinct.

    Timeline example

    Imagine a product catalog page with a cached entry for product SKU-42.

    Text
    T0  cache contains price=100
    T1  writer updates DB price to 120
    T2  reader A hits cache and sees 100
    T3  writer invalidates cache
    T4  reader B misses cache and reloads 120

    At T2, the cached value was already stale from a business perspective, even though it was still technically present. That is the stale-data window.

    Choosing the right strategy

    If the data changes rarely and read traffic is heavy, absolute expiration may be enough, especially with explicit invalidation on writes. Sliding expiration removes idle entries, but frequent reads can keep a hot entry eligible indefinitely. Combine it with an absolute expiration when you need a maximum cache lifetime. If a decision requires current authoritative data, a short TTL is not enough. Read the authoritative state or use a consistency protocol whose guarantees meet that decision. Treat any tolerated stale interval as an explicit business choice.

    Be careful with these assumptions:

    • A shorter expiration does not guarantee zero staleness.
    • A sliding timeout can keep very hot stale data around longer than expected if invalidation is delayed.
    • Distributed cache APIs generally provide storage and retrieval, not distributed locking.
    • In-process cache and distributed cache have very different failure and visibility characteristics.

    Interview drill

    MCQ 1: Which expiration strategy best limits the maximum time a value can remain cached, even if it is read constantly?

    A. Sliding expiration only B. Absolute expiration only C. No expiration D. Random expiration

    Correct answer: B. Absolute expiration sets a fixed lifetime ceiling. Sliding expiration can extend life on every read.

    MCQ 2: A writer has committed a new value to the database but has not yet deleted the old cache entry. Why can a concurrent reader still return the old value?

    A. Because invalidation is always slower than reads B. Because the reader can hit the old cache entry during the gap before deletion C. Because cache entries cannot be deleted D. Because databases automatically repopulate the cache

    Correct answer: B. The read and write operations are not synchronized as one atomic action.

    MCQ 3: Which statement is most accurate about caching in a backend system?

    A. The cache should be treated as the source of truth B. Authorization checks can be skipped if the cache is warm C. The cache is an optimization and the authoritative store still matters D. Sliding expiration guarantees fresh data

    Correct answer: C. Caching improves performance, but it does not replace correctness checks or ownership of truth.

    Failure modes

    1. Too-long absolute expiration: stale catalog data survives longer than business tolerates.
    1. Overreliance on sliding expiration: hot but obsolete entries stay alive because they keep getting read.
    1. Write-then-invalidate gap: after the database commit but before cache deletion, a concurrent reader can still hit the old entry.
    1. Read-then-write race: a late cache fill writes old data after the database has already been updated.
    1. Confusing in-process with distributed caching: local memory cache and shared cache have different visibility, eviction, and coordination behavior.

    The practical fix is usually a combination of shorter lifetimes, explicit invalidation, versioned keys, and clear acceptance of a small stale window when absolute freshness is not feasible.

    Revision checklist

    • I can explain the difference between absolute expiration and sliding expiration.
    • I can say when to use a hard lifetime cap versus access-based renewal.
    • I can describe a stale-data window using a read/write timeline.
    • I understand that invalidation reduces, but does not eliminate, races.
    • I know the cache is an optimization and not a replacement for the source of truth.
    • I can distinguish in-process cache behavior from distributed cache behavior.
    • I can explain why cache freshness and authorization are separate concerns.

    Worked exercises: deadlines and stale refills

    With IMemoryCache, deadline correctness is different from immediate physical reclamation. Expired entries must not be served on lookup; cache activity can trigger cleanup. Do not test correctness by waiting for an eviction callback. Microsoft: memory-cache behavior.

    1. Predict hit or miss

    Consider three independent entries inserted at minute 0: absolute-only with a deadline at minute 10; sliding-only with a four-minute idle limit; and combined with both limits. Look up each at minutes 3, 6, 9 and 11. Assume no eviction, removal, replacement or refill; a miss leaves the entry absent.

    1. Minutes 3, 6 and 9: all three hit. Each successful access is three minutes after the previous one, so the sliding deadline moves to minutes 7, 10 and 13.
    1. Minute 11: absolute-only and combined miss because their fixed deadline was minute 10. Sliding-only hits because the last successful access was at minute 9.
    1. Practice: instead, suppose the first lookup after insertion is at minute 5. Answer: absolute-only hits; sliding-only and combined miss because they were idle for more than four minutes.

    These are predictions under the stated rules, not a recorded framework run. To test an implementation, control time and assert lookup results; do not infer expiry from callback timing.

    Next, trace the stale-refill case. Reader A captures the old source value and pauses before cache insertion. Writer B commits the new value and removes the old cache entry. A then resumes and inserts its captured old value. A completed removal cannot prevent a later insertion. This is different from the existing timeline, where the stale read happens before removal.

    2. Diagnose the late fill

    Use the existing prices: A captures 100; B commits 120 and removes the cache entry; A then publishes its captured 100. The next cache hit returns 100. Question: did removal fail? Answer: no. The stale value came from a later insertion.

    Design target: no stale generation may become the current cached generation after the corresponding local write completes. A proposed fix must coordinate the generation check and publication; checking a version and then inserting outside the coordinated operation reopens the race. A schema prefix such as v1 is not a data-generation protocol.

    Implementation exercise: assert the hit/miss sequence and force the order of capture, commit, removal and late fill. For a proposed protected design, assert its generation invariant too. Multiple processes, replicas and real database commit visibility require their own protocol; the paper exercise proves no distributed consistency property. Microsoft: cache-aside consistency considerations.

    Optional reading: Cache correctness companion note (Full Access). The worked exercises above are self-contained.

    Analogy

    Everyday picture

    Imagine a study-room pass with two limits. One gives it a fixed closing time. The other expires after 20 minutes without use and restarts whenever the pass is used. Under these rules, frequent use extends the idle timer but never moves the fixed closing time.

    Mapping. The fixed closing time represents absolute expiration. The idle timer represents sliding expiration. When both rules apply, reaching either limit makes the pass invalid.

    Where it stops. A valid pass says nothing about whether the room's noticeboard is up to date. Likewise, expiration does not coordinate a database write, cache invalidation and a concurrent refill. Removing an old notice while another clerk is carrying a copy can still put that old copy back on display.

    Sign in to mark lessons done and keep your place in the course.Sign in