A team added a Redis cache because “the site is slow.” A week later the support queue filled up: “I updated the price but the old one still shows.” The cache had fixed the slowness — and quietly created a new problem.

Adding a cache is not a free performance win. It trades freshness for speed; and if you didn’t make that trade deliberately, the bill comes back as a bug.

A cache is a consistency concession

The moment you cache a value, you’ve accepted this: someone, for a while, may see stale data. A cache is never a “faster database”; it’s “speed in exchange for accepted staleness.”

So the first question isn’t technical: how stale can this data be? Data whose answer is “not at all” — an account balance, a stock count — should be thought through twice before it’s cached.

Questions to ask first

Before you turn on Redis:

  • Is this really a read bottleneck? “The site is slow” is a hypothesis, not a measurement. Adding a cache without seeing the source of the slowness is covering up something you don’t understand.
  • Is the slowness actually a missing index? A cache papers over a bad query but doesn’t fix it. Hiding behind a cache what a CREATE INDEX would solve is moving the problem, not solving it — this is exactly the wrong breaking point in data-intensive systems.
  • How stale can this data stay? The answer determines which strategy you pick.

Why invalidation is hard

Cache invalidation gets cited as one of the two hard problems in software — and that’s no exaggeration. Putting the cache in is easy; clearing it at the right moment is hard.

Because a cached value must be cleared on every write path that affects it. A product price changes not only from the “edit product” screen; it also changes from a bulk price update, a discount job, an admin script. Finding all of those paths and clearing the cache — that’s the hard part. Miss one, and you get the support ticket from the top of this post.

Two strategies

In practice there are two roads:

  • TTL-based. You assign the value a lifetime — 60 seconds, 5 minutes. When it expires, the cache refreshes itself. Simple, sturdy, and it doesn’t require knowing every write path. In return: you accept staleness up to the TTL.
  • Explicit invalidation. You delete the cache by hand when the data changes. Exact and fresh — but only if you catch every write path completely.

My default is TTL. It’s boring, predictable, and it doesn’t break silently because of a forgotten write path. I move to explicit invalidation only when staleness is genuinely unacceptable and the number of write paths is limited and known. Most of the time the two are used together: a short TTL as a safety net, explicit invalidation for speed.

A cache doesn’t reduce writes

A common mistake: trying to rescue a system under write load by adding a cache. A cache reduces read load; it has no effect on write load — if anything, invalidation itself is extra write work. If your problem is on the write side, a cache is the wrong tool.

Practical patterns

  • Cache-aside. Read: check the cache first, and on a miss fetch the value from the database and write it to the cache. The most common and most understandable pattern.
  • Stampede protection. The instant a popular key’s TTL expires, hundreds of requests hit the database at once. On a cache miss, use a short lock so only one request goes to the database.
  • Key discipline. Namespace your cache keys — the pattern in the shared Redis namespace isolation note applies to cache keys too.

A cache is not the cure for a slow system; it’s the relief of a measured read bottleneck, bought with a deliberate consistency concession. A cache added without an invalidation plan takes back more as debt than it speeds up.

Before you add a cache, ask: how old can this data be — and who’s going to clear it?