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 INDEXwould 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?

Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.