Redis Caching Best Practices and Pitfalls
The caching module covered the patterns and their failure modes one at a time. This article pulls them into a practical set of habits and warnings, the things experienced teams do by default and the mistakes that catch everyone at least once. None of it is complicated, but each item corresponds to a production incident someone had to debug, so treating them as defaults saves you from repeating the lesson.
This is part 15 and the last of the caching module in the Redis Masterclass, following cache stampede.
Cache the right things
Caching adds complexity, so it should earn its place. The data worth caching is read far more than it's written and expensive to produce: a query joining several tables, a computed aggregate, a rendered fragment, a rarely-changing config. Caching data that changes on every request, or that's trivial to fetch, adds moving parts for little gain. Before caching something, ask whether it's actually read-heavy and actually slow to fetch; if not, skip it.
The corollary is don't cache everything reflexively. Each cached key is another thing to invalidate correctly and another consumer of memory. A smaller cache of genuinely hot data is easier to keep correct than a sprawling one caching things that barely benefit.
Always set a TTL
Give every cache key a TTL, even when you also invalidate explicitly. The TTL is your backstop: it bounds staleness, self-cleans abandoned keys, and rescues you when (not if) you forget to invalidate somewhere. A cache key with no expiry is a future bug, either a memory leak or a permanently stale value. The only keys without a TTL should be genuinely persistent data, and that probably shouldn't be in the cache namespace at all.
Design your keys deliberately
Key naming isn't cosmetic. A consistent, hierarchical scheme (entity:id:field, like user:1:profile) keeps the keyspace understandable, makes invalidation groupable, and prevents collisions. Decide the convention early and apply it everywhere. Include a version or schema marker in keys that cache structured data, so a format change (adding a field to a cached object) doesn't serve old-shaped data to new code; bumping the version effectively invalidates the old format.
Handle the cache being down
Your application must survive Redis being unavailable. With cache-aside this is mostly free (misses fall through to the database), but only if your code treats a Redis error as a miss rather than crashing. Wrap cache reads so a Redis failure logs and falls through to the source, never propagates as a 500. Also watch the flip side: if Redis is down and every request falls through, the database takes the full load it was shielded from, so a cache outage can cascade into a database overload. Rate limiting and the stampede protections help here.
The pitfalls that catch people
A roundup of the specific mistakes worth naming:
- Caching without invalidation planning. Adding a cache is easy; keeping it correct is the work. Decide the invalidation strategy when you add the cache, not after the first stale-data bug.
- No TTL, or the same TTL on everything. Missing TTLs leak memory and stale data; identical TTLs on a batch cause synchronized-expiry stampedes. Set TTLs, and jitter them.
- Caching user-specific data under a shared key. Caching a personalized response under a non-user-scoped key serves one user's data to another. Include the user or tenant id in the key for anything personalized. This is both a correctness and a security bug.
- Storing huge values. A multi-megabyte cached value is slow to transfer and hogs memory. Cache what's needed, not entire object graphs, and watch value sizes.
- Ignoring memory limits. Without
maxmemoryand an eviction policy, Redis fills RAM and gets killed. Set both, as covered in the TTL article. - Trusting the cache as a source of truth. The database is authoritative. If a value exists only in the cache and nowhere else, its loss is data loss. Cache copies of durable data, not the only copy.
Monitor the cache
You can't tune what you don't measure. The key metric is hit rate: hits divided by total lookups. A low hit rate means the cache isn't helping (wrong data cached, TTLs too short, or keys too fragmented) and is pure overhead. Also watch memory usage against maxmemory, eviction counts (high evictions mean the cache is too small or holding too much), and latency. Redis exposes these via INFO, and monitoring them tells you whether the cache is doing its job. We'll cover monitoring in depth in the operations module.
The short version
If you take a handful of habits from this module: cache read-heavy, expensive data and nothing else; give every key a TTL with jitter; scope keys to the user or tenant when data is personalized; treat a cache outage as a fall-through, not a crash; and monitor your hit rate. Those defaults prevent the large majority of caching incidents, and each one exists because someone learned it the hard way.
Caching is one of the highest-value things Redis does, and also one of the easiest to get subtly wrong. The patterns give you the tools; these practices keep you from the common traps. Cache deliberately, expire always, scope carefully, degrade gracefully, and measure the result.
That closes the caching module. Next, we move into using Redis as infrastructure, starting with pub/sub messaging for real-time communication between parts of your system.
Key takeaways
- Cache only data that's read-heavy and expensive to produce; caching everything adds complexity and invalidation risk for little gain.
- Give every cache key a TTL as a backstop, even with explicit invalidation, and add jitter to avoid synchronized expiry.
- Use a consistent hierarchical key scheme, and scope keys to the user or tenant for personalized data to avoid leaking it.
- Treat a Redis outage as a fall-through to the database, not a crash, and be aware the database then takes the full load.
- Monitor hit rate, memory, and evictions; a low hit rate means the cache is overhead rather than help.
Frequently asked questions
What data should I cache in Redis?
Data that's read far more than it's written and expensive to fetch or compute, like multi-table query results, aggregates, and rarely-changing config. Avoid caching data that changes constantly or is trivial to fetch, since the invalidation complexity outweighs the benefit.
Should every cache key have a TTL?
Yes. A TTL bounds staleness, self-cleans abandoned keys, and rescues you when you forget to invalidate somewhere. A key with no expiry risks becoming a memory leak or a permanently stale value. Add jitter so keys cached together don't expire simultaneously.
What happens if Redis goes down?
With cache-aside and proper error handling, requests fall through to the database and the app keeps working, just slower. Make sure your code treats a Redis error as a cache miss rather than crashing, and note the database then absorbs the load the cache was shielding.
How do I avoid serving one user's cached data to another?
Include the user or tenant id in the cache key for any personalized data. Caching a per-user response under a shared key is both a correctness bug and a security leak.
How do I know if my cache is effective?
Monitor the hit rate (hits divided by total lookups). A high hit rate means the cache is doing its job; a low one means it's overhead, often from caching the wrong data, TTLs that are too short, or overly fragmented keys. Also watch memory and eviction counts.

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.