Four caches, and knowing which one you need
HTTP, fragment, low-level and query caching solve different problems. Reaching for the wrong one hides the bug instead of fixing it.
Caching is the most common answer to "make it faster" and the least often applied deliberately. Rails gives you four distinct mechanisms and they are not interchangeable.
The one that avoids the request entirely
HTTP caching is the cheapest possible win because the fastest request is the one your
server never handles. Set an ETag or Last-Modified and a conditional request returns
304 with no body — the framework skips rendering entirely.
fresh_when and stale? make this a one-liner in a controller. For content that
changes rarely and is read constantly, this is the highest-leverage caching in the
stack, and it is routinely skipped in favour of something more complicated.
Fragment caching, and the Russian doll
Fragment caching stores rendered markup keyed on a record and its updated_at. Touch
the record, the key changes, the fragment regenerates. No manual expiry, which is the
part that makes it safe.
Nesting fragments — the Russian doll pattern — means an inner change invalidates its own
fragment and every wrapper above it, while siblings stay cached. It works beautifully as
long as touch: true is set on the associations, and fails silently and confusingly when
it is not.
Low-level caching for things that are not markup
Rails.cache.fetch is for expensive values: an aggregate, an external API response, a
computed report. You own the key and the expiry, which is both the power and the danger.
Two failure modes are worth naming. Keys that include something per-request — a timestamp, a request id — never hit, and the code looks completely correct in review. And unbounded key spaces, where a key includes user input, quietly fill the cache with entries that will never be read again.
Query caching is smaller than people think
Active Record caches identical queries within a single request. That is the whole scope — it is gone at the end of the request and does nothing across requests.
It is useful, and it is not a caching strategy. If you are relying on it, what you actually have is repeated queries in one request, and the fix is to not issue them.
Caching a slow query does not make it fast. It makes it intermittently fast and much harder to reason about.
Order of operations
Fix the query first. Cache second. This ordering is not pedantry: a cached slow query is a landmine that goes off on every cache miss, at the worst possible time, when the cache is cold after a deploy and traffic is at its peak.
When I cut clinical-study page loads by more than half, the caching work came after the N+1s and the missing counter cache. Caching first would have hidden the problem and produced a system that was fast on average and occasionally catastrophic.
The invalidation part
The cliché is that cache invalidation is hard. In Rails, most of that difficulty is self-inflicted through manual expiry.
Key-based expiry sidesteps it: put whatever the content depends on into the key, and stale entries become unreachable rather than wrong. They age out on their own. You never write a line of invalidation logic, and you never get the class of bug where a delete path forgot to clear something.
The rule I follow is that if I am writing Rails.cache.delete anywhere, I have probably
designed the key badly.