Caching (In-Memory, Distributed & Redis)

15 questions found

What is IMemoryCache, and when is it appropriate to use for caching in ASP.NET Core?

Beginner
IMemoryCache is a built-in in-process cache storing key-value pairs directly in the application's own memory, ideal for single-instance applications or data that's acceptable to recompute per-instance -- it's extremely fast (no network round-trip) but not shared across multiple server instances, and its contents are lost on application restart.
builder.Services.AddMemoryCache();

public class ProductService {
    private readonly IMemoryCache _cache;
    public ProductService(IMemoryCache cache) => _cache = cache;

    public async Task<Product> GetProductAsync(int id) {
        return await _cache.GetOrCreateAsync($"product-{id}", async entry => {
            entry.SlidingExpiration = TimeSpan.FromMinutes(10);
            return await _repository.GetByIdAsync(id);
        });
    }
}
Real-world example A single-instance internal reporting tool caches expensive aggregate query results in IMemoryCache for 10 minutes, dramatically reducing database load for a report viewed repeatedly by the same handful of users.

Common follow-ups: What's the memory pressure risk of IMemoryCache with unbounded growth?;How does IMemoryCache differ across multiple load-balanced server instances?

Diagnostics & Performance;Configuration & Options

What is the difference between absolute expiration and sliding expiration when configuring a cache entry?

Intermediate
Absolute expiration removes an entry at a fixed point in time regardless of how recently it was accessed, guaranteeing data won't become stale beyond a hard limit. Sliding expiration resets the expiration timer every time the entry is accessed, keeping frequently-used data cached indefinitely while letting rarely-used entries expire -- the two can be combined to set both a 'refresh on access' behavior and a hard maximum lifetime.
_cache.Set("key", value, new MemoryCacheEntryOptions {
    SlidingExpiration = TimeSpan.FromMinutes(5),      // resets on each access
    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)  // hard cap regardless
});
Real-world example A frequently-accessed user session cache uses a 5-minute sliding expiration combined with a 1-hour absolute cap, so active users keep their cached session alive while inactive sessions are forcibly cleared within an hour at most.

Common follow-ups: What happens if only sliding expiration is set with no absolute cap?;How does cache eviction interact with memory pressure signals?

Diagnostics & Performance;Authentication & Authorization (Identity JWT OAuth)

What is IDistributedCache, and how does it differ from IMemoryCache in a multi-instance deployment?

Advanced
IDistributedCache is an abstraction over an external, shared cache store (like Redis or SQL Server) that all instances of a horizontally-scaled application read from and write to consistently, unlike IMemoryCache where each instance maintains its own separate, unsynchronized in-process cache. This ensures cache coherency across multiple servers/pods -- a value cached by one instance is immediately visible to all others -- at the cost of network latency per cache operation compared to in-memory access.
builder.Services.AddStackExchangeRedisCache(options => {
    options.Configuration = "localhost:6379";
});

public class ProductService {
    private readonly IDistributedCache _cache;
    public async Task<Product?> GetProductAsync(int id) {
        var cached = await _cache.GetStringAsync($"product-{id}");
        if (cached != null) return JsonSerializer.Deserialize<Product>(cached);
        // ... fetch and cache
    }
}
Real-world example A horizontally-scaled API running behind a load balancer with 10 instances uses Redis-backed IDistributedCache so a product cached by instance A is immediately available to instances B through J, avoiding 10x redundant database queries for the same data.

Common follow-ups: Why does IDistributedCache only work with byte[]/string, unlike IMemoryCache's object storage?;What's the latency trade-off of every cache access requiring a network call?

Microservices & Distributed Architecture Patterns;Diagnostics & Performance

How do you configure and use Redis as a distributed cache in ASP.NET Core with StackExchange.Redis?

Intermediate
Install the Microsoft.Extensions.Caching.StackExchangeRedis package, register it via AddStackExchangeRedisCache with a connection string, then inject IDistributedCache to get/set values as byte arrays or strings (requiring manual serialization for complex objects, typically with System.Text.Json), or use IConnectionMultiplexer directly for more advanced Redis-specific features like pub/sub or sorted sets.
builder.Services.AddStackExchangeRedisCache(options => {
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "MyApp:";
});

// Usage
await cache.SetStringAsync("key", JsonSerializer.Serialize(data),
    new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });
Real-world example A microservices architecture uses a shared Redis instance as the distributed cache layer for session data, feature flags, and computed aggregates, letting all services benefit from consistent, centrally-managed cache invalidation.

Common follow-ups: What's the difference between using IDistributedCache versus IConnectionMultiplexer directly?;How does InstanceName help avoid key collisions when multiple apps share one Redis instance?

Microservices & Distributed Architecture Patterns;Configuration & Options

What is the 'cache stampede' (thundering herd) problem, and how can you mitigate it?

Advanced
A cache stampede occurs when a popular cache entry expires and many concurrent requests simultaneously discover the cache miss, all racing to recompute and repopulate the same expensive value at once, overwhelming the backing data source. Mitigations include: using a lock/semaphore per cache key to ensure only one request recomputes while others wait, using GetOrCreateAsync's built-in coordination (IMemoryCache handles this internally for the same key), or proactively refreshing cache entries before they fully expire.
private static readonly SemaphoreSlim _lock = new(1, 1);

public async Task<Product> GetProductAsync(int id) {
    if (_cache.TryGetValue($"product-{id}", out Product cached)) return cached;
    await _lock.WaitAsync();
    try {
        if (_cache.TryGetValue($"product-{id}", out cached)) return cached;  // double-check
        var product = await _repository.GetByIdAsync(id);
        _cache.Set($"product-{id}", product, TimeSpan.FromMinutes(10));
        return product;
    } finally { _lock.Release(); }
}
Real-world example A viral news article's cache entry expiring under heavy simultaneous traffic previously caused 500 concurrent database queries for the same article; adding a per-key lock reduces this to exactly one database query while the other 499 requests wait briefly for the now-cached result.

Common follow-ups: How does IMemoryCache.GetOrCreateAsync handle this internally?;What is 'stale-while-revalidate' as an alternative mitigation strategy?

Diagnostics & Performance;Concurrency (asyncio/threading/multiprocessing)

What are common cache invalidation strategies, and why is cache invalidation considered a genuinely hard problem?

Intermediate
Common strategies include: TTL-based expiration (simplest, but stale data possible until expiry), explicit invalidation (removing/updating the cache entry immediately when the underlying data changes, requiring careful tracking of every code path that mutates data), and versioned/tagged cache keys (invalidating whole groups of related entries at once by bumping a version number embedded in the key). It's hard because every single place data can change must remember to invalidate the correct cache entries, and distributed systems make coordinating invalidation across multiple cache instances even trickier.
// Explicit invalidation on write
public async Task UpdateProductAsync(Product product) {
    await _repository.UpdateAsync(product);
    await _cache.RemoveAsync($"product-{product.Id}");  // must remember this every time!
}

// Versioned key strategy
var cacheKey = $"products-v{await GetCacheVersionAsync()}";
Real-world example A bug where stale product prices were shown for hours was traced to a bulk-update code path that updated the database directly via a script, bypassing the normal service method that would have invalidated the corresponding cache entries.

Common follow-ups: Why is 'there are only two hard things in computer science: cache invalidation and naming things' such a common joke?;How do cache tags/groups simplify bulk invalidation?

Diagnostics & Performance;Entity Framework Core & Data Access

How does HybridCache (introduced in .NET 9) combine in-memory and distributed caching to solve common pain points?

Advanced
HybridCache provides a unified API that transparently layers a fast local in-memory cache in front of a distributed cache (like Redis), automatically handling cache stampede protection (built-in request coalescing for concurrent identical requests) and serialization, giving you both the speed of local caching and the consistency of distributed caching without hand-rolling the two-tier logic yourself.
builder.Services.AddHybridCache();

public class ProductService {
    private readonly HybridCache _cache;
    public async Task<Product> GetProductAsync(int id) {
        return await _cache.GetOrCreateAsync($"product-{id}", async ct => {
            return await _repository.GetByIdAsync(id, ct);
        }, new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(10) });
    }
}
Real-world example A team previously hand-rolling a two-tier cache (check IMemoryCache, fall back to Redis, populate both on miss) replaces dozens of lines of custom coordination logic with a single HybridCache.GetOrCreateAsync call after upgrading to .NET 9.

Common follow-ups: How does HybridCache handle stampede protection across multiple server instances, not just within one process?;What serialization options does HybridCache support for complex objects?

Diagnostics & Performance;Configuration & Options

What is response caching middleware in ASP.NET Core, and how does it differ from application-level data caching?

Intermediate
Response caching (via UseResponseCaching middleware and [ResponseCache] attributes) caches entire HTTP responses (based on cache-control headers, varying by query string/headers) at the HTTP layer, serving cached responses directly without even invoking the controller action again -- distinct from application-level data caching (IMemoryCache/IDistributedCache), which caches specific pieces of data used within your business logic, potentially still requiring the full request pipeline to execute.
builder.Services.AddResponseCaching();
app.UseResponseCaching();

[ResponseCache(Duration = 60, VaryByQueryKeys = new[] { "category" })]
[HttpGet]
public IActionResult GetProducts(string category) => Ok(_products.Where(p => p.Category == category));
Real-world example A public product catalog API caches entire GET /products?category=electronics responses for 60 seconds using response caching middleware, avoiding the full controller execution (including data-layer caching) entirely for repeated identical requests within that window.

Common follow-ups: How does VaryByQueryKeys affect what counts as a 'unique' cached response?;How does response caching interact with a CDN sitting in front of the API?

ASP.NET Core Middleware & Request Pipeline;RESTful Web APIs & Controllers

How does Redis's data structure support (beyond simple key-value) enable caching patterns not possible with IMemoryCache?

Advanced
Redis natively supports rich data structures -- sorted sets (for leaderboards or rate limiting windows), hashes (for efficiently updating individual fields of a cached object without rewriting the whole entry), sets (for fast membership/intersection operations), and pub/sub (for cache invalidation broadcast across multiple app instances) -- letting you implement caching patterns like 'top 10 most-viewed products, updated incrementally' that would require significant custom logic with a simple key-value cache like IMemoryCache.
// Using IConnectionMultiplexer directly for Redis-specific features
var db = _redis.GetDatabase();
await db.SortedSetIncrementAsync("product-views", productId.ToString(), 1);
var topProducts = await db.SortedSetRangeByScoreWithScoresAsync("product-views", order: Order.Descending, take: 10);
Real-world example A real-time 'trending products' feature uses a Redis sorted set incremented on every product view, letting the top-10 leaderboard be queried instantly without any separate aggregation job, a pattern that's awkward to implement with plain key-value caching.

Common follow-ups: What is Redis pub/sub and how does it enable cross-instance cache invalidation?;When would you reach for raw IConnectionMultiplexer instead of the IDistributedCache abstraction?

Microservices & Distributed Architecture Patterns;Diagnostics & Performance

What is cache-aside (lazy loading) pattern, and how does it compare to write-through caching?

Intermediate
Cache-aside (the most common pattern) has the application check the cache first, and on a miss, load from the data source and populate the cache for next time -- the cache is only ever updated reactively on read misses. Write-through caching instead updates the cache synchronously every time data is written to the underlying store, guaranteeing the cache is never stale for writes that went through this path, at the cost of added write latency and more complex write-path code.
// Cache-aside (most common)
public async Task<Product> GetProductAsync(int id) {
    var cached = await _cache.GetAsync(id);
    if (cached != null) return cached;
    var product = await _db.GetProductAsync(id);
    await _cache.SetAsync(id, product);
    return product;
}

// Write-through: cache updated on every write, not just read-miss
public async Task UpdateProductAsync(Product product) {
    await _db.UpdateAsync(product);
    await _cache.SetAsync(product.Id, product);  // cache updated immediately
}
Real-world example A read-heavy product catalog uses cache-aside since reads vastly outnumber writes, while a financial ledger system uses write-through caching to guarantee the cache never briefly shows stale balance data after a transaction.

Common follow-ups: What is write-behind (write-back) caching and when is it appropriate?;How does cache-aside handle the stampede problem differently than write-through?

Entity Framework Core & Data Access;Diagnostics & Performance

Showing 1–10 of 15