Q19 — Caching Layers — IMemoryCache, IDistributedCache, HybridCache, Output Cache
Level: Senior | Topic: Performance Optimization
Choosing the Right Cache
- IMemoryCache: In-process, zero serialisation, microsecond latency. Single-instance only — cache is lost on restart and not shared across pods.
- IDistributedCache (Redis): Shared across all instances, survives restarts. Requires serialisation. Use for multi-instance deployments.
- HybridCache (.NET 9): L1 (in-memory) + L2 (Redis) with built-in stampede protection. The modern default for multi-instance apps.
- Output Cache (.NET 7+): Caches the full HTTP response at the middleware level. Tag-based invalidation, vary by route/header. No application code needed per endpoint.
Code Example
// 1. IMemoryCache — single node, zero serialisation
var products = await _memCache.GetOrCreateAsync("products:all", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
entry.SlidingExpiration = TimeSpan.FromMinutes(3);
entry.Priority = CacheItemPriority.High;
return await repo.GetAllAsync(ct);
}) ?? [];
// 2. IDistributedCache (Redis) — multi-instance
var key = $"user:{userId}";
var bytes = await _distCache.GetAsync(key, ct);
if (bytes is not null)
return JsonSerializer.Deserialize<UserDto>(bytes);
var user = await repo.GetByIdAsync(userId, ct);
await _distCache.SetAsync(key,
JsonSerializer.SerializeToUtf8Bytes(user),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
}, ct);
// 3. HybridCache (.NET 9) — L1 memory + L2 Redis + stampede protection
var product = await _hybridCache.GetOrCreateAsync(
$"product:{id}",
async token => await _repo.GetByIdAsync(id, token),
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30), // L2 Redis TTL
LocalCacheExpiration = TimeSpan.FromMinutes(5) // L1 Memory TTL
},
tags: [$"product", $"product:{id}"], // tag-based invalidation
cancellationToken: ct);
// Invalidate by tag (removes from both L1 and L2)
await _hybridCache.RemoveByTagAsync($"product:{id}", ct);
// 4. Output Cache (Program.cs) — response-level caching
builder.Services.AddOutputCache(opts =>
{
opts.AddBasePolicy(b => b.Expire(TimeSpan.FromSeconds(30)));
opts.AddPolicy("ByUserId", b => b.VaryByRouteValue("userId").Expire(TimeSpan.FromMinutes(5)));
});
app.UseOutputCache();
// On controller action:
[HttpGet, OutputCache(PolicyName = "ByUserId", Tags = new[] { "user" })]
public async Task<IActionResult> GetUser(string userId) { ... }
Senior Insight
Cache stampede (dog-pile) is when a cache expiry causes dozens of concurrent requests to all hit the database simultaneously. HybridCache handles this automatically. With IMemoryCache, implement a SemaphoreSlim-based locking pattern manually. Always define TTLs conservatively — stale data bugs are harder to debug than cache misses. Use tag-based invalidation to purge related cache entries atomically when an entity is updated.