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}:lowstock
Flat, 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
- Profile first. Cache the endpoints that are frequent and expensive, nothing else.
- Scope keys to the tenant and every parameter that changes the result.
- Combine tuned TTLs with event-driven eviction so fresh data stays fresh.
- Prove 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.
Originally published at huzaifaawan.com. I am Muhammad Huzaifa Awan, a Senior Full Stack Developer building SaaS, AI products, and MCP servers. More writing and my portfolio live at huzaifaawan.com.
Top comments (0)