Redis cache-aside

The application reads the cache first, loads the database on a miss, and deletes the key when the data changes.

Assembling the machine…

Listing

  1. // api · products.ts
  2. async function getProduct(id) {
  3. const key = `product:${id}`;
  4. const hit = await redis.get(key);
  5. if (hit) return JSON.parse(hit);
  6. const row = await db.selectProduct(id);
  7. const json = JSON.stringify(row);
  8. await redis.set(key, json, { EX: 60 });
  9. return row;
  10. }
  11. async function setPrice(id, price) {
  12. await db.updatePrice(id, price);
  13. await redis.del(`product:${id}`);
  14. }

Step 1 / 6

A page asks for product 42

GET /products/42 reaches the API. The API, not Redis, decides where the product comes from.

Event log · 1
  1. 0.00 msHTTPGET /products/42 → API

The concept

The application reads the cache first, loads the database on a miss, and deletes the key when the data changes.

Cache-aside, also called lazy loading, is a caching pattern in which the application, not the cache, keeps the cache filled: it reads the cache first, and on a miss it reads the database, stores the result in the cache with an expiry, and returns it. Redis is a common cache for it, an in-memory data store that never queries a database by itself.

On a write, the application updates the database first, then deletes the cached key rather than writing the new value into it. The next read misses and loads the new value. A delete is idempotent; values written into the cache by several writers can land in the wrong order.

The expiry is what bounds staleness. Anything that changes the database without deleting the key, an import job, a manual fix, another service, leaves the old value in the cache until the key expires. A key set without an expiry lives until someone deletes it or Redis evicts it, and the default policy evicts nothing: once maxmemory is reached, Redis refuses new writes.

Cache-aside does not keep the cache consistent with the database. Between a write and its delete, readers see the old value, and a slow read that overlaps a write can put the old value back after the delete. The expiry is the upper bound on how long that lasts.

A hot key that expires under load sends every request that misses to the database at once: a cache stampede. Common fixes let one caller reload the key while the others wait or keep serving the old value: a short lock taken with SET NX, coalescing requests inside a process, or recomputing a little before the expiry. Redis offers none of them as such: the application does it.

The run, step by step

Every scenario, in words.

What the machine plays, step by step. Beside a step, what a what-if switch plays in its place, or adds right after it.

Read a product

6 steps
  1. Play

    A page asks for product 42

    GET /products/42 reaches the API. The API, not Redis, decides where the product comes from.

  2. Play

    Redis first: a miss

    GET product:42 returns nil: nothing is cached yet. Redis does not go and look for it: the API must.

    • With Expire a hot key under load

      Play

      A hot key expires under load

      product:42 has just expired, and 50 requests ask for it within a few milliseconds. Every GET misses.

    • With Cache without an expiry + Expire a hot key under load

      Play

      A hot key is deleted under load

      A price change has just deleted product:42, and 50 requests ask for it within a few milliseconds. Every GET misses.

  3. Play

    On a miss, the database

    The API reads the product from the database: 10 €. Slower than Redis, and the reason the cache exists.

    • With Expire a hot key under load

      Play

      50 identical queries

      Nothing tells the requests that one of them is already loading the key: each runs the same SELECT. The database takes 50 queries for one row: the very load the cache was there to absorb.

  4. Play

    Cache it for 60 seconds

    SET product:42 … EX 60 stores the product with an expiry. However the price changes, this copy lasts 60 s at most.

    • With Cache without an expiry

      Play

      Cache it, with no expiry

      SET without EX: the key will live until someone deletes it. Nothing goes wrong yet.

    • With Expire a hot key under load

      Play

      50 SETs of the same value

      Every request caches what it loaded: 50 writes of the same product. The cache ends up right; the database paid 50 times for it.

  5. Play

    The page gets 10 €

    The API returns the product it loaded. The page cannot tell whether it came from Redis or the database.

  6. Play

    The next read: a hit

    Another page asks for product 42. GET finds it in memory: the database is not touched.

    • With Expire a hot key under load

      Play

      Let one request load it

      The usual fix lets one caller reload the key while the others wait: a short lock with SET … NX, or requests coalesced inside each process. Redis does not do it by itself.

Change a price

4 steps
  1. Play

    The back office changes the price

    Staff set product 42 to 12 €. The API handles the write too, and must clear the cached copy.

    • With Delete the key before writing · adds a step after it

      Play

      The read puts 10 € back

      The slow read finishes and caches the price it read. Pages show 10 € while the database says 12 €, for up to 60 s: until the key expires or the next write deletes it.

    • With Cache without an expiry + Delete the key before writing · adds a step after it

      Play

      The read puts 10 € back, for good

      The slow read caches the price it read, with no expiry. Nothing expires it: pages show 10 € until the next write deletes the key.

  2. Play

    Database first: UPDATE

    The new price is committed. Until the next line runs, Redis still holds 10 €, and readers can get it.

    • With Delete the key before writing

      Play

      The key goes first

      setPrice deletes product:42 before it touches the database. The database still says 10 €.

  3. Play

    Then delete the key

    DEL product:42 rather than SET: the next read loads the new price. Deleting twice is the same as deleting once, and two writers' deletes cannot leave the older price behind.

    • With Delete the key before writing

      Play

      A read slips in

      Before the UPDATE commits, a page asks for product 42. The key is gone, so the API reads the database: 10 €.

  4. Play

    The next read loads 12 €

    A page asks for product 42: a miss, the database answers 12 €, and the API caches it again.

    • With Delete the key before writing

      Play

      Then the UPDATE commits

      The database now says 12 €. The read that loaded 10 € has not finished yet.

Change it behind the cache

3 steps
  1. Play

    An import job writes the database

    A nightly import sets product 42 to 12 €, straight in the database. It knows nothing about Redis, so nothing deletes the key.

  2. Play

    Reads keep getting 10 €

    Every page hits the cache and gets the old price. Redis is not wrong by its own rules: no one told it.

  3. Play

    The key expires; 12 € loads

    Within 60 s the TTL runs out and Redis deletes the key. The next read misses and loads 12 €. The TTL bounded the damage.

    • With Cache without an expiry

      Play

      It never expires

      SET without EX made the key permanent: TTL -1. The old price stays until someone deletes the key, and by default Redis never evicts it to make room.