The situation
A dashboard API took 900ms at p95. The database was healthy, the queries were indexed, the instance was not under pressure. Nobody could explain why it was slow.
Nobody had ever looked at the cache. When we finally did, the hit ratio was 31% — which is worse than having no cache at all, because you pay for the lookup and get a miss.
Why a bad cache is slower than no cache
Every request paid a network round-trip to Redis, serialised a result, wrote it back, then missed on the next request and hit PostgreSQL anyway. The cache was pure overhead plus a serialisation cost.
Three changes
- Key by shape, not by URL.
dash:{tenantId}:{range}:{filtersHash}instead of the raw query string. Half the keys were one-off variants nobody ever requested twice. - TTL with jitter. A flat 60-second TTL means every key expires in the same second. Adding +/-20% random jitter smoothed the miss curve completely.
- Stampede protection. A single-flight lock so that when a popular key expires, one request repopulates it and the rest wait rather than all hammering the database.
Result: hit ratio 96%, p95 from 900ms to 120ms, database connections down 70%.
The lesson
If you have a cache, measure it on day one. INFO stats gives you keyspace_hits and keyspace_misses for free. A cache nobody measured is a cache nobody tuned — and usually worse than no cache at all.