You Don't Need Redis Yet: Cache Invalidation Is a Tax, Not a Feature
Adding a cache is not a performance upgrade. It is a trade.
EFFICIENCY
Anant Dhavale with Claude
7/9/20264 min read
Adding a cache is not a performance upgrade. It is a trade: faster reads in exchange for taking on cache invalidation, the obligation for every write path in your system to know which cached answers it just made wrong. For most applications under a few thousand users reading indexed queries from Postgres, that trade loses. This article explains why, when the trade actually wins, and how to tell the difference before you pay the tax.
Where does cache staleness actually come from?
Staleness is a policy you wrote, not a property of the cache. Redis serves whatever you put in it, for exactly as long as you told it to. Cache a query result with a 30-minute TTL and you have chosen 30 minutes of staleness. Set it to 30 seconds and you have chosen 30 seconds. When someone says "the dashboard is 30 minutes stale because of Redis," they are describing their own cache policy and attributing it to the tool.
This matters because it reframes the decision. You are not choosing whether to have a fast layer. You are choosing how wrong your reads are allowed to be, and for how long, and then signing up to enforce that choice on every write forever.
What is the real cost of adding a cache?
Cache invalidation: the moment you cache derived results, every write path must know every cache entry it invalidates, and missing one means serving a wrong answer with full confidence.
Take a workflow system with review queues and roll-up dashboards. A reviewer approves a task. That single write invalidates her review queue, the submitter's inbox, the team's obligation roll-up, and the executive dashboard. Four cache entries, one write, and the mapping between them lives in your application code, maintained by hand, updated every time a new view is added. Miss one and a user sees a status that is no longer true.
For some products that is a cosmetic lag. For systems of record, compliance status, financial positions, approvals, inventory, it is a wrong answer delivered quickly. If your architecture derives status from source data precisely so it can never drift, a cache layer deliberately reintroduces the staleness you designed out, in exchange for speed you likely don't need.
Why don't you need the speed?
Because at small-to-mid scale, indexed Postgres reads are already faster than your users can perceive, and Postgres caches hot data in memory on its own.
Run the numbers for a typical internal or B2B application: a few hundred users, mostly reading short, filtered lists. A lookup on a proper composite index, say tenant, owner, and status, returns in single-digit milliseconds. Postgres keeps frequently accessed pages in its buffer cache, so hot queries are already served from memory without you managing anything. There is no realistic load in that profile where a Redis layer produces a difference a human notices, and every load where it produces an invalidation bug a human definitely notices.
The rule of thumb: add a cache when you have a measured slow query that indexing cannot fix. Never preemptively. A cache added before the problem exists is a solution shopping for a problem, and it shops on your operational budget.
What should you do instead when things feel slow?
Work through the boring list, in order, because it solves essentially every performance problem a typical application hits in its first years.
Indexes. Most "we need a cache" moments are a missing composite index. Check the query plan first.
Connection pooling. Connection churn masquerades as database slowness. Pool before you conclude Postgres is the bottleneck.
Async for slow externals. If the slow part is a third-party API call, caching your own database does nothing. Move the external call off the request path.
Pagination. Unbounded list queries get slower forever. Bounded ones don't.
Each of these is cheaper than a cache, none of them can serve a stale answer, and together they cover the actual failure modes of read-heavy CRUD systems at this scale.
When does Redis legitimately enter the stack?
When you need what Redis is actually for, and notably, none of it is caching your query results.
Job queues. When background work outgrows an in-process scheduler and thread pool, Redis as the broker for Celery, or with RQ, is a standard, sane choice. Though even here, Postgres can run a perfectly good queue using the SKIP LOCKED pattern, fine at moderate throughput, with a dedicated queue earning its place only at high job volumes where table bloat and vacuum pressure start to bite. One database is operationally simpler than two.
Rate limiting and sessions. Once real authentication and public traffic arrive, fast shared counters and session storage are a legitimate fit.
Genuinely hot, genuinely tolerant reads. If you eventually have a measured hot path, indexing is exhausted, and the data can tolerate defined staleness, cache that one thing, with a deliberate TTL and an explicit invalidation map. A cache adopted for one named problem is maintainable. A cache adopted as architecture is a tax on every future write.
Summary
Redis is a fine tool and a bad reflex. Staleness is a policy you set, invalidation is a permanent obligation you accept, and at the scale most applications actually operate, indexed Postgres already delivers reads faster than users can perceive. Derive-don't-store architectures exist so the system can never quietly serve yesterday's truth; a preemptive cache trades that guarantee away for nothing. Exhaust the boring list, index, pool, async, paginate, and let a cache in only when a measured problem, not a habit, invites it.
Frequently asked questions
Isn't caching a best practice for scalability? At large scale, often yes. The mistake is importing large-scale architecture into small systems, where the invalidation risk arrives immediately and the performance benefit never does.
My dashboard is slow. Isn't that the case for Redis? Check the query plan first. Dashboard slowness at moderate scale is almost always a missing index, an unbounded query, or a slow external call on the request path, none of which a cache fixes correctly.
Does this apply to derived or computed values too? Especially to them. Derived values are exactly what invalidation gets wrong, because many writes affect them indirectly. Deriving at read time from indexed source data keeps them correct by construction.
What metric tells me it's finally time? A specific query, measured in production, that stays slow after correct indexing and bounding, on a path where defined staleness is acceptable. Cache that query. Not the application.
-----
Notes:
Homer Semantics builds compliance and governance systems where a stale answer is a wrong answer, which keeps our architectural bias simple: derive, don't store, and earn every layer of infrastructure. Write to info@homersemantics.com to talk architecture.
If you are looking for specific AI Implementation services, do check out https://procors.com/
Read a few other useful articles at : https://www.homersemantics.com/blog-list
This website may use essential and third-party cookies for embedded media, basic site functionality, and performance monitoring.
