Caching means storing the result of expensive work so you can reuse it instead of redoing it. The trade-off, always, is speed in exchange for the risk of serving something out of date — which is why it isn't free and shouldn't be the reflex answer to every slow thing.
Where caching happens in a typical web stack, from the user inward:
1. **Browser cache** — static files (CSS, JS, images) stored on the user's device so repeat visits don't re-download them. Controlled by HTTP cache headers. Cheapest win available.
2. **CDN** — copies of your static assets on servers near users worldwide, cutting the physical distance the data travels.
3. **Application cache** — expensive computed results or rendered pages held in memory or in something like Redis.
4. **Database query cache** — results of heavy queries kept so repeated identical queries skip the work.
When it's the right tool:
- The data is read far more often than it changes (a product catalogue, a blog post, a settings lookup, an expensive report).
- The computation or query is genuinely expensive.
- Slightly stale data is acceptable for the use case.
When it isn't:
- Data that must be exactly current — account balances, stock levels at checkout, permissions.
- Data unique to each user with no reuse, where you'd cache a million entries each read once.
- **As a substitute for fixing the underlying problem.** This is the big one. If a query takes four seconds because it lacks an index or does an N+1 loop, caching hides a bug rather than fixing it — and now you have a slow query *and* a staleness problem. Fix the query first; cache after it's already reasonable.
The hard part, which is where the famous joke comes from: invalidation. Deciding *when* to throw cached data away is genuinely difficult, and the failure modes are confusing — a user updates their profile and still sees the old name, or two servers disagree because one's cache expired and the other's didn't. Strategies range from simple time-based expiry (easy, predictable, sometimes stale) to explicit invalidation on write (accurate, easy to get wrong when several code paths can modify the data).
Practical advice: start with time-based expiry and a short TTL. It's simple, it's bounded, and 'stale for at most 60 seconds' is acceptable far more often than people assume. Move to explicit invalidation only where correctness genuinely requires it.
And measure before and after. Caching adds real complexity — another system to run, another failure mode, another source of confusing bugs. If it isn't producing a measurable improvement on a real bottleneck, it's making the codebase worse for nothing.