A product page loads quickly, but its price stays wrong after the shop updates it. You clear Redis, refresh the browser, and still see the old price because another cache holds a copy. Caching makes repeated work cheaper, but every copy creates a question: when is it safe to reuse? Understanding where copies live and how they change helps you build faster systems without trusting outdated data.

A cache is a convenient copy

A cache stores a reusable copy of data or a result to avoid fetching or calculating it again. Think of writing a formula on a study card instead of searching your textbook each time. The card saves time, but an outdated formula can give you the wrong answer.

In an application, the database usually remains the source of truth: the authoritative record. Cached data should be replaceable from that source.

A cache hit means the requested item is present and usable. A cache miss means it must be fetched or computed. Caching reduces latency, the time a request takes, and database work. It also consumes memory, adds code, and can return stale data: a copy that no longer matches the source.

Follow one product through three cache layers

Our shop sells product 42, a USB keyboard priced at NPR 1,500. Its public page contains a photo and product details. Checkout must verify the database’s current price and stock before accepting an order.

These three caches store different things:

Layer Where it lives What it stores in our shop

Browser cache The visitor’s device Previously downloaded photos, scripts, or responses CDN cache Servers distributed across locations Public photos and eligible web responses shared across visitors Redis cache A service accessed by the application Product data under a key such as product:42

A content delivery network, or CDN, serves content from distributed servers, often closer to visitors. The origin is the shop’s server behind that CDN. Redis is a data store that keeps working data in memory and supports operations on named entries called keys.

Browser cache | miss v CDN cache | miss v Application --> Redis cache | | miss +-------------+ v Database

For product details:

  1. The browser reuses a usable cached response or makes a request.
  2. The CDN checks for a reusable response. A hit avoids an origin request.
  3. On a CDN miss, the application checks Redis.
  4. On a Redis miss, the application reads the database, caches that data, and returns it.

Each layer needs its own rules; Redis does not control browser or CDN caching.

Tell browsers and CDNs what they may reuse

HTTP, the Hypertext Transfer Protocol, defines messages between web clients and servers. Response headers carry information such as caching instructions.

For public product details, an illustrative response is:

HTTP/1.1 200 OK Content-Type: application/json Cache-Control: public, max-age=30, s-maxage=60 ETag: "product-42-v3"

{"id":42,"name":"USB keyboard","price_npr":1500}

JSON, JavaScript Object Notation, is a text format for structured data. Here, Cache-Control permits shared caching with a 30-second browser freshness lifetime and 60 seconds for shared caches. Freshness means a response may be reused without checking the origin. HTTP accounts for its existing age; downloading from a CDN does not restart its freshness clock.

Instruction Meaning Suitable example

max-age=30 Freshness lifetime of 30 seconds Briefly reusable public product details s-maxage=60 Shared-cache lifetime; overrides max-age there A CDN’s public product response private Shared caches must not store the response A visitor’s account page no-cache Storage is allowed, but reuse requires successful validation A page that must be checked each time no-store Caches must not store this response Sensitive account or payment data

Validation checks whether a stored response is current. An ETag, or entity tag, identifies the response content’s version. The browser can send:

If-None-Match: "product-42-v3"

If the version matches, the server can return 304 Not Modified without the product body. The browser reuses its stored body. Validators must change when the content changes.

Check your CDN’s configuration: its settings can override these rules. Avoid public for personalized responses. no-store does not erase copies stored under an earlier policy.

Cache-aside versus write-through

HTTP rules govern cached responses. The application still needs a strategy for its Redis data.

Cache-aside: fetch when needed

With cache-aside, the application manages the cache alongside the database. For product 42, it reads Redis first. On a miss, it reads the database and stores the result with an expiry.

When the price changes to NPR 1,400, a common sequence is to commit, or finalize, the database change, then delete product:42. The next reader rebuilds that entry. Invalidation means making a cached copy unusable because it may be outdated.

This caches requested items, but a miss requires both a database read and storing the result.

Write-through: update on every write

With write-through, a write updates the backing database and cached value before reporting success. This is synchronous: the caller waits for both. A cache layer or application wrapper coordinates these operations.

For the keyboard, an implementation could commit the new price, then cache NPR 1,400. This avoids a deliberate miss after successful updates, but also caches unread products.

Question Cache-aside Write-through

When is data populated? When a read misses During writes; reads may still need miss handling Typical update action Commit database, invalidate entry Update database and cached value Write cost Database work plus invalidation Database work plus cache update Main difficulty Miss handling and stale copies Coordinating failures and concurrent writes

Writing to two systems does not create one atomic transaction, an operation that succeeds or fails as a whole. If the database succeeds but Redis fails, write-through still needs a failure policy. Concurrent writes can also reach the cache out of order.

Write-back updates the cache first and the database later. Pending writes need reliable storage and recovery to prevent data loss.

TTLs: decide how long a copy lasts

A time to live, or TTL, limits how long an entry remains usable before expiration. For Redis product data, a 60-second TTL means the key expires after 60 seconds unless another operation changes its expiry.

Choose TTLs from the consequences of stale data. A photo tolerates a long lifetime; displayed prices may need short ones. Checkout must enforce authoritative price and stock when the database accepts the purchase.

Longer TTLs improve reuse but increase exposure to stale copies; shorter TTLs increase database traffic. Start with the business’s acceptable delay, then measure request times, hit rates—the fraction of lookups that hit—and database load.

Expiration differs from eviction, removal to free memory. Depending on its configured memory policy, Redis may evict an entry before its TTL ends. Your code must handle misses at any time.

For static files, content served unchanged between updates, a content-derived filename such as keyboard.a81f.jpg allows a long lifetime: changes produce a different URL, or web address. This is versioning. The referencing page still needs an appropriate freshness policy.

Why invalidation is hard

Deleting one key sounds simple. Keeping every copy aligned while requests overlap is harder.

Consider this race condition, where the outcome depends on operations’ timing:

Reader: loads old price 1500 from database Writer: commits new price 1400 Writer: deletes Redis entry Reader: stores its old price 1500 in Redis

The writer invalidated the entry, yet the reader recreated stale data. Deleting before committing introduces another race: a reader may refill the cache before the database changes.

Commit-then-delete plus a TTL is useful, but does not guarantee immediate consistency. Consistency here means how closely different copies agree with the authoritative record. More demanding systems may coordinate readers and writers or use version-aware updates that reject older values.

Failures add another problem: the database update may succeed while invalidation fails. Reliable retry processing can repair that failure, but does not solve every concurrency race.

Finally, deleting Redis does not purge the CDN or a browser’s fresh response. A CDN purge removes selected stored responses; browser copies generally depend on HTTP rules or changed URLs. Layered caches can extend staleness beyond one layer’s TTL, so design the whole path.

Hands-on: Redis commands and Python

Start a local Redis server

With Docker installed, run:

docker run --name psc-redis -p 127.0.0.1:6379:6379 -d redis:7 docker exec -it psc-redis redis-cli

Docker runs Redis in a container, an isolated environment for the server, and prints its identifier. redis-cli opens Redis’s command-line client. Binding to 127.0.0.1 limits published access to this machine.

Enter these commands; their replies appear underneath:

SET psc:demo:ttl '{"id":42,"price_npr":1500}' EX 60 OK

GET psc:demo:ttl "{"id":42,"price_npr":1500}"

TTL psc:demo:ttl (integer) 59

EXPIRE psc:demo:ttl 120 (integer) 1

DEL psc:demo:ttl (integer) 1

TTL psc:demo:ttl (integer) -2

The first TTL reply varies with elapsed time. SET ... EX 60 stores the value and expiry atomically; GET retrieves it; EXPIRE changes an existing key’s lifetime; DEL removes it. TTL returns remaining seconds, -1 for an existing key without expiry, or -2 for a missing key. A normal GET does not extend the TTL. A plain SET replacing a value removes its previous expiry unless you supply an expiry or explicitly preserve it.

Run a cache-aside example

Leave the client with exit. With Python 3 installed, run these Bash commands. The virtual environment isolates this project’s Python packages:

python3 -m venv .venv source .venv/bin/activate python -m pip install redis

Save this as cache_demo.py. SQLite provides the database through Python’s standard library. :memory: makes it temporary; each run seeds it again.

import json import sqlite3

import redis from redis.exceptions import RedisError

cache = redis.Redis( host="localhost", port=6379, decode_responses=True, socket_connect_timeout=0.2, socket_timeout=0.2, )

db = sqlite3.connect(":memory:") db.row_factory = sqlite3.Row db.execute( "CREATE TABLE products " "(id INTEGER PRIMARY KEY, name TEXT, price_npr INTEGER)" ) db.execute("INSERT INTO products VALUES (?, ?, ?)", (42, "USB keyboard", 1500)) db.commit()

def cache_key(product_id): return f"psc:demo:v1:product:{product_id}"

def invalidate(product_id): try: cache.delete(cache_key(product_id)) except RedisError: print("warning: invalidation failed; cached data may be stale")

def get_product(product_id): try: cached = cache.get(cache_key(product_id)) except RedisError: cached = None

if cached is not None:
    print("cache hit")
    return json.loads(cached)

print("cache miss")
row = db.execute(
    "SELECT id, name, price_npr FROM products WHERE id = ?",
    (product_id,),
).fetchone()
if row is None:
    return None

product = dict(row)
try:
    cache.set(cache_key(product_id), json.dumps(product), ex=60)
except RedisError:
    pass  # The database result is still usable.
return product

def update_price(product_id, price_npr): with db: db.execute("UPDATE products SET price_npr = ? WHERE id = ?", (price_npr, product_id)) invalidate(product_id) # Runs after the transaction commits.

invalidate(42) # Clear this demo's key before each run. print(get_product(42)) print(get_product(42)) update_price(42, 1400) print(get_product(42))

Run it:

python cache_demo.py

With Redis running, the expected output is:

cache miss {'id': 42, 'name': 'USB keyboard', 'price_npr': 1500} cache hit {'id': 42, 'name': 'USB keyboard', 'price_npr': 1500} cache miss {'id': 42, 'name': 'USB keyboard', 'price_npr': 1400}

decode_responses=True returns strings instead of raw bytes. Timeouts limit waits while connecting or exchanging data. Cache errors allow database reads to continue; failed invalidation warns that stale copies may remain. Production systems need failure monitoring and reliable invalidation retries. This sequential demo does not solve concurrent reader/writer races.

Remove the demo container when finished:

docker rm -f psc-redis

Choosing caching or an alternative

Situation Reasonable approach

Repeated public photos and scripts Browser caching and a CDN Repeated product lookups with acceptable staleness Redis with cache-aside and deliberate TTLs Frequently read records that are also updated Consider write-through with explicit failure handling Final price, stock reservation, or permission checks Enforce correctness against authoritative data Rarely repeated queries or already fast reads Measure before adding a cache

Alternatives include improving database queries, adding indexes, structures that help the database find matching rows, or precomputing expensive summaries. An application-local cache avoids a Redis network call, but separate application processes then hold separate copies.

Common mistakes and their fixes

  1. Caching private data in shared responses. Use suitable private or no-store policies. Check each requester’s permission, called authorization; cache keys do not replace access checks.
  2. Leaving entries without expiry. Set value and TTL together. Remember that overwriting with plain SET drops the previous TTL.
  3. Ignoring cache outages. Use short timeouts, track failures, and plan database capacity for misses. Otherwise, fallback traffic can overwhelm the database.
  4. Letting many requests rebuild one entry. A cache stampede happens when requests simultaneously rebuild missing data. Coordinate rebuilding per key. Random TTL variation spreads different keys’ expirations but cannot alone protect one popular key.
  5. Treating deletion as guaranteed freshness. Account for overlapping reads, failed invalidations, and browser/CDN copies. Bypass cached data for decisions requiring the latest authoritative state.
  6. Exposing Redis or allowing unlimited growth. Use private networks, authentication to verify identity, and encryption to protect traffic. Set memory limits and an appropriate eviction policy.

Quick recap

  • A cache is a replaceable copy; choose the authoritative source first.
  • Browser caches, CDNs, and Redis operate at different layers.
  • Cache-aside fills on misses; write-through updates during writes.
  • TTLs limit entry lifetime, but do not solve every freshness problem.
  • Invalidation needs a plan for concurrency, failures, and multiple layers.

Test yourself

  1. Does Cache-Control: no-cache forbid storage?

Answer: No. It permits storage but requires successful validation before reuse. no-store forbids storing the response.

  1. What does Redis TTL returning -1 mean?

Answer: The key exists without an expiry. -2 means the key does not exist.

  1. Why can stale data appear after commit-then-delete?

Answer: An overlapping reader may have fetched the old database value before the commit and write it into Redis after deletion.

Where to go next

  • HTTP validation and response variants: Learn ETag and Vary, the header that tells caches which request headers affect the response, in MDN’s HTTP caching guide.
  • Database transactions and isolation: Study how transactions coordinate concurrent changes, especially stock reservations.
  • Redis memory management: Explore Redis eviction policies and monitor hit rates, memory use, and failures.