How I Hit a 95% Redis Cache Hit Rate Under Real Load
The profiling, cache-key design, and invalidation strategy that took a .NET SaaS API to sub-50ms responses and a 95% Redis cache hit rate under 100+ concurrent users.
On a multi-tenant SaaS dashboard I work on, response times climbed as concurrency grew. The fix was not a bigger server. It was a Redis caching strategy layered onto a .NET Core API. The result: a 95% cache hit rate and sub-50ms responses under 100+ concurrent users. Here is the actual process, not the highlight reel.
Measure before you cache
Caching the wrong thing hides problems and creates new ones. I started by profiling the API under realistic load to find the true hotspots: the endpoints that were both frequently hit and expensive to compute. Almost all the pain came from a handful of dashboard reads that ran heavy SQL Server aggregations on every request.
- Frequent + expensive = cache it.
- Frequent + cheap = probably leave it.
- Rare + expensive = optimize the query, do not cache.
Cache-key design is the whole game
A cache is only as good as its keys. For a multi-tenant app, every key has to be scoped to the tenant and to the exact parameters that change the result: date range, filters, locale. Get this wrong and you either leak data across tenants or get a hit rate near zero because no two keys ever match.
dashboard:{tenantId}:kpis:{from}:{to}:{locale}
dashboard:{tenantId}:queue:live
catalog:{tenantId}:lowstockFlat, predictable, and self-documenting. When a key names exactly what it holds, invalidation becomes obvious instead of guesswork.
Invalidation without the footguns
The famous hard problem. I leaned on two techniques: short, tuned TTLs for data that is allowed to be a few seconds stale (KPI roll-ups), and event-driven invalidation for data that must be fresh: when a write happens, the specific keys it affects are evicted immediately. Mixing the two keeps the hit rate high without ever showing a user stale-but-important data.
Verify the hit rate is real
A 95% hit rate only means something if you measure it under production-like concurrency, not a single-user test. I tracked hits, misses, and p95 latency while ramping concurrent users, and watched the cache warm and hold. That is also how you catch a key that looks cached but never actually matches.
The takeaways
- 1Profile first. Cache the endpoints that are frequent and expensive, nothing else.
- 2Scope keys to the tenant and every parameter that changes the result.
- 3Combine tuned TTLs with event-driven eviction so fresh data stays fresh.
- 4Prove the hit rate under real concurrency, not on your laptop.
Performance work like this, turning a slow app that scales poorly into a fast one, is a big part of what I do. See more of my work or reach out if your app is starting to feel the load.
Written by
Muhammad Huzaifa Awan
Senior Full Stack Developer building scalable web apps, AI-powered SaaS and enterprise systems. Available for senior roles and select freelance work.
Work with meKeep reading
Connecting Claude to Your Outlook Inbox and Calendar with MCP
What it takes to let an AI assistant safely read, draft, and send email and book meetings: the architecture behind Atlas, my Outlook MCP server for Claude.
How I Built an MCP Server Exposing 60+ Tools to Claude
Architecting a production MCP server that gives Claude secure access to 60+ real tools: the auth, transport, and security decisions that actually mattered.