ASP.NET Core Caching Interview Questions for Senior .NET Developers (2026)
Caching is one of the most frequently tested topics in senior .NET interviews โ and with good reason. Done well, caching transforms sluggish APIs into high-throughput systems. Done poorly, it becomes the source of stale data, cache stampedes, memory leaks, and production incidents that take hours to debug. If you are preparing for a senior or lead-level .NET role, expect your interviewer to go well beyond "what is IMemoryCache?" and push into distributed strategies, invalidation logic, and architectural trade-offs.
This guide covers the ASP.NET Core caching interview questions that actually appear in technical screens in 2026 โ grouped by difficulty from foundational through expert-level โ with clear, direct answers you can study and internalize.
๐ Want implementation-ready .NET source code you can drop straight into your project? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. ๐ https://www.patreon.com/CodingDroplets
Basic-Level Questions
What is the difference between IMemoryCache and IDistributedCache in ASP.NET Core?
IMemoryCache stores cached data in the process memory of a single application instance. It is fast, simple to use, and ideal for single-server deployments or non-critical caching scenarios. IDistributedCache is an abstraction over a shared cache that lives outside the application process โ typically Redis or SQL Server โ making it suitable for multi-instance, load-balanced environments where all nodes need access to the same cached data.
The key operational difference: if your app runs behind a load balancer with multiple replicas, IMemoryCache will give different results on different nodes because each instance maintains its own in-memory state. IDistributedCache solves this by centralising the cache store.
What is the Cache-Aside pattern and why is it used?
The Cache-Aside pattern (also called Lazy Loading) means the application is responsible for loading data into the cache on demand. The logic is: check the cache first; if the data is there, return it (cache hit); if not, fetch it from the database, store it in the cache, then return it (cache miss).
This is the most commonly used caching strategy in ASP.NET Core applications because it keeps the cache lean โ only data that is actually requested gets cached โ and gives the application full control over what is cached and when.
What are absolute expiration and sliding expiration in ASP.NET Core caching?
Absolute expiration sets a fixed point in time when the cached item expires, regardless of how often it is accessed. Once that timestamp is reached, the item is removed from cache. Use this when data has a known staleness window (e.g., a report that refreshes hourly).
Sliding expiration resets the expiration timer each time the item is accessed. If an item is not accessed within the sliding window, it expires. This is useful for session-like data where active users should always have their data in cache, but inactive data should be evicted automatically.
You can combine both on the same cache entry: the sliding expiration keeps recently accessed items alive, while the absolute expiration acts as a hard ceiling to prevent an item from living in cache indefinitely.
What is the purpose of MemoryCacheEntryOptions and what are its key properties?
MemoryCacheEntryOptions controls the lifecycle and priority of a cached item in IMemoryCache. Key properties include:
- AbsoluteExpiration โ exact time when the entry expires
- AbsoluteExpirationRelativeToNow โ expiration offset from the time the item is added
- SlidingExpiration โ resets on each access; entry expires if not accessed within the window
- Priority โ controls eviction priority under memory pressure (
Low,Normal,High,NeverRemove) - Size โ required when you set a
SizeLimiton the cache; used for capacity management - PostEvictionCallbacks โ callbacks invoked when the entry is evicted, useful for logging or triggering a refresh
What caching providers does IDistributedCache support out of the box in ASP.NET Core?
ASP.NET Core ships with three built-in implementations of IDistributedCache:
- In-memory distributed cache (
AddDistributedMemoryCache) โ a single-node in-process cache that is only suitable for development and testing. Despite the name, it does not actually distribute across nodes. - SQL Server distributed cache (
AddDistributedSqlServerCache) โ stores cached data in a SQL Server table. Durable, but higher latency than Redis. - Redis distributed cache (
AddStackExchangeRedisCache) โ the production-grade choice for high-throughput distributed caching. Requires a running Redis instance.
Third-party libraries such as NCache and Garnet extend this abstraction further.
Intermediate-Level Questions
What is a cache stampede and how do you prevent it in ASP.NET Core?
A cache stampede (also called thundering herd) happens when a high-traffic cache entry expires and many concurrent requests all reach the database simultaneously to repopulate it. This can overwhelm the database and cause a cascade failure.
Prevention strategies in .NET:
- Locking with SemaphoreSlim โ use a semaphore to allow only one request to populate the cache while others wait for the result. Release the semaphore once the cache is populated.
- Probabilistic early expiration โ expire items slightly before their actual expiry based on a probability function, so background refresh starts before the item expires for all users.
- Background cache refresh โ use a hosted service or a
IMemoryCachepost-eviction callback to proactively refresh hot cache entries before they expire. - Stale-while-revalidate โ serve the stale item immediately while refreshing it in the background. Redis and HTTP output caching support this natively.
What is Output Caching in ASP.NET Core and how does it differ from Response Caching?
Output Caching was introduced in ASP.NET Core 7 and stores the full serialized response on the server side. The cache is controlled by the server and is not dependent on client or proxy behaviour. You can define custom policies, vary the cache by query string, route values, headers, or custom keys, and you can programmatically invalidate entries by tag.
Response Caching relies on HTTP caching headers (Cache-Control, Vary, ETag, Last-Modified) and is primarily a client and proxy caching mechanism. The server does not store the response itself; it instructs downstream caches (browsers, CDNs, reverse proxies) on how to cache it.
For server-side API response caching in modern ASP.NET Core applications, Output Caching is the preferred approach. Response Caching is more relevant when you are optimising CDN or browser cache behaviour.
How do you handle cache invalidation in a distributed environment?
Cache invalidation in distributed systems is notoriously difficult. Common strategies in .NET:
- TTL-based expiration โ the simplest strategy. Let entries expire on a time-to-live basis and accept a window of stale data. Reliable but not precise.
- Event-driven invalidation โ publish a cache invalidation event (via a message bus like MassTransit or Azure Service Bus) when the underlying data changes. All nodes subscribe and evict or refresh the relevant cache entry.
- Tag-based invalidation with Output Caching โ ASP.NET Core Output Caching supports cache tags. When data changes, call
IOutputCacheStore.EvictByTagAsync(tag)to invalidate all cached responses associated with that tag. - Write-through invalidation โ update the cache synchronously whenever the database is written. Ensures consistency but increases write latency.
In practice, most production systems use TTL-based expiration as the baseline combined with event-driven invalidation for critical entities.
What are the memory pressure and eviction mechanisms in IMemoryCache?
IMemoryCache supports size-based eviction when you configure a SizeLimit. Each cache entry must declare its Size, and the cache evicts entries when the total size limit is approached. Eviction uses a combination of priority and least-recently-used (LRU) approximation.
Eviction triggers:
- Explicit removal via
IMemoryCache.Remove(key) - Expiration (absolute or sliding)
- Capacity pressure โ when the cache approaches its
SizeLimit, it evicts lower-priority entries first - Host memory pressure โ the .NET runtime can trigger eviction through
MemoryCache.Compact(percentage)when system memory is under pressure
Entries with CacheItemPriority.NeverRemove are exempt from capacity-based eviction, so use this sparingly. Without SizeLimit, the cache will grow unbounded, which is a common source of memory leaks in production.
How does Redis key expiration work and what caching patterns does it enable?
Redis supports two expiration mechanisms:
- TTL expiration โ set with
EXPIREorEXPIREATcommands. The key is removed when the TTL elapses. - Passive expiration โ expired keys are only removed when they are next accessed. Combined with periodic active sampling, Redis maintains memory without guaranteeing instant removal.
In .NET, StackExchange.Redis allows you to set TimeSpan TTLs directly. When using IDistributedCache, the DistributedCacheEntryOptions.AbsoluteExpirationRelativeToNow maps to a Redis TTL.
Redis also supports more advanced patterns relevant to senior-level interviews:
- Pub/Sub for cache invalidation โ publish invalidation messages to subscribers when data changes
- Redis keyspace notifications โ subscribe to key expiry events, useful for triggering cache refresh workflows
- Redis Cluster and partitioned caching โ distribute cache data across multiple nodes for horizontal scalability
Advanced-Level Questions
How do you design a multi-level caching strategy in ASP.NET Core?
A multi-level (or tiered) caching strategy uses multiple cache layers with different characteristics:
L1 โ In-Process Memory Cache (IMemoryCache): Sub-millisecond access, local to the instance. Ideal for frequently accessed, small, read-heavy data (reference data, config lookups, user permission sets). Short TTL (30โ300 seconds) to avoid stale divergence across instances.
L2 โ Distributed Cache (Redis): Shared across all instances. Higher latency than L1 but consistent. Longer TTL (minutes to hours). Used for session data, expensive query results, aggregated reports.
L3 โ Database or origin: The source of truth. Only hit on full cache misses through both L1 and L2.
The read path: check L1 โ if miss, check L2 โ if miss, query the database โ populate L2 โ populate L1 โ return result.
The write path on cache invalidation: evict or update L2 โ broadcast L1 eviction signal via pub/sub or accept short stale window.
This strategy reduces Redis round-trips for hot data while keeping consistency across scaled-out instances.
How do you implement cache warming in ASP.NET Core?
Cache warming pre-populates the cache before it receives live traffic to avoid cold-start latency spikes after a deployment. In ASP.NET Core, this is typically implemented using IHostedService or BackgroundService registered to run during application startup. The warming service fetches critical datasets from the database and loads them into the distributed or in-memory cache during StartAsync.
For output caches or HTTP-level caches, warming is done by issuing internal HTTP requests to the API endpoints after startup. Azure Front Door and Varnish support cache warming through crawler configuration.
Key concerns for production warm-up:
- Do not block the HTTP pipeline; let the health check return "starting" or "degraded" until warming completes.
- Rate-limit database queries during warming to avoid overloading the database on every deployment.
- Use
IHostApplicationLifetime.ApplicationStartedtoken to coordinate warm-up with the host lifecycle.
What is the "cache stampede with probabilistic early expiration" technique?
Probabilistic early expiration (PER), described in the XFetch algorithm, avoids stampedes by proactively refreshing a cache entry before it actually expires, based on a probability that increases as the entry approaches its expiration time.
The formula is: currentTime - delta * beta * log(rand()) >= expiryTime
Where delta is the time it took to compute the cached value and beta is a tuning constant (default 1.0). When this condition is true for any incoming request, that request triggers a background refresh โ before the entry expires โ while other requests continue receiving the valid cached value. This naturally distributes the refresh load rather than causing a simultaneous stampede at expiry.
In .NET, this pattern is implemented by storing the computation time alongside the cached item and evaluating the formula on each cache read. It is particularly valuable for high-traffic endpoints where entry expiry would trigger thousands of simultaneous database queries.
How do you test caching logic in ASP.NET Core unit and integration tests?
Unit testing with IMemoryCache: Use MemoryCache (the concrete class) directly or mock IMemoryCache with a library like NSubstitute. The concrete class is preferred for integration-style unit tests since it behaves like the real implementation.
Unit testing with IDistributedCache: Use MemoryDistributedCache (the in-memory implementation) in tests. This avoids Redis dependency while using the same interface.
Integration testing: Use WebApplicationFactory<T> and register a test-scoped IDistributedCache using AddDistributedMemoryCache. This provides full-stack integration tests without a running Redis instance. For Redis-dependent scenarios, use Testcontainers for .NET to spin up a real Redis container per test run.
Key scenarios to test:
- Cache hit path returns the cached value without calling the database
- Cache miss path populates the cache and calls the database exactly once
- Expiration behaviour (advance the clock using a fake time provider in tests)
- Concurrent access does not cause duplicate database calls (test the semaphore lock under load)
How does ASP.NET Core Output Caching handle cache key variation and what are the best practices for API caching policies?
Output Caching computes a cache key from a set of vary-by factors configured in the cache policy. By default, the key includes the request method and path. You can add vary-by rules for:
- Query string parameters โ
policy.VaryByQuery("page", "pageSize") - Route values โ
policy.VaryByRouteValue("id") - HTTP headers โ
policy.VaryByHeader("Accept-Language") - Custom values โ implement
IOutputCachePolicyto vary by any request property, including the authenticated user identity
Best practices for API output cache policies:
- Apply output caching only to GET and HEAD requests; never cache POST/PUT/DELETE responses.
- Tag every cached response with entity tags (
policy.Tag("products")) so invalidation by tag is possible on writes. - Use short TTLs (5โ60 seconds) for frequently changing data and longer TTLs for stable reference data.
- For authenticated APIs, vary by user identity or do not apply output caching to personalised responses โ caching personalised responses can leak private data to other users.
- Combine output caching with a CDN vary-by strategy so edge nodes cache appropriate variations without over-fragmenting the cache.
Expert-Level Questions
How does the RedLock algorithm relate to distributed cache locking in .NET?
RedLock is an algorithm for implementing distributed locks on top of Redis, designed to handle the failure of Redis nodes. A naive distributed lock acquires a lock on a single Redis instance, but if that instance fails between the lock acquisition and release, the lock is never released and the system deadlocks.
RedLock acquires the lock on a majority (N/2 + 1) of independent Redis nodes within a short time window. A lock is considered acquired only if the client successfully locks the majority of nodes and the time elapsed acquiring the lock is less than the lock's TTL. On release, the client releases the lock on all nodes.
In .NET, RedLock.net provides a client implementation. Senior engineers are expected to understand when RedLock is necessary (truly distributed, multi-node Redis without Redis Cluster Redlock support) versus when a simpler single-node lock (SET ... NX EX) is sufficient.
What are the trade-offs between using a write-through versus write-behind caching strategy in enterprise .NET systems?
Write-through updates the cache synchronously on every write operation. The cache and the database are always consistent. The trade-off is higher write latency because both the database write and the cache write must complete before the operation returns. This is the safer choice for financial or transactional systems where stale cache is unacceptable.
Write-behind (write-back) writes to the cache immediately and defers the database write to a background process. This gives low-latency writes but introduces a window where the cache holds data not yet persisted to the database. If the application crashes during that window, writes are lost unless there is a durable write queue.
In enterprise .NET systems, write-behind is implemented using a message queue (e.g., Azure Service Bus or Redis Streams) as the buffer between the cache write and the database write. The background worker consumes the queue and writes to the database asynchronously. This is common in high-write-volume scenarios such as user activity tracking, analytics ingestion, or gaming leaderboards โ where eventual consistency is acceptable.
Frequently Asked Questions
What is the difference between IMemoryCache and a static dictionary in C#?
IMemoryCache is a purpose-built cache with built-in support for expiration, eviction under memory pressure, size limits, thread-safe concurrent access, and post-eviction callbacks. A static dictionary has none of these capabilities โ it will grow unbounded, has no expiry mechanism, requires manual locking for thread safety, and cannot respond to memory pressure. Never use a static dictionary as a cache in production; always use IMemoryCache or IDistributedCache.
When should you use IDistributedCache over IMemoryCache?
Use IDistributedCache when your application runs as multiple instances (e.g., behind a load balancer or in Kubernetes), when cached data must be shared across instances, or when the cache must survive application restarts. Use IMemoryCache when you have a single instance, require sub-millisecond access latency, and the data does not need to be shared. Multi-level caching combines both.
How do you prevent caching sensitive or personalised data?
For output caching, apply NoStore() on any policy associated with authenticated or personalised endpoints. For IMemoryCache, cache only non-personalised aggregate data and avoid keying entries with user-specific data unless you explicitly scope the cache key to the user identity and accept the memory overhead. For IDistributedCache, encrypt sensitive cached values using IDataProtectionProvider if the cache store is shared or hosted outside your trust boundary.
What causes stale cache issues in distributed systems and how do you mitigate them?
Stale cache arises when the backing data changes but the cache entry has not been invalidated or refreshed. Root causes include: long TTLs relative to the rate of data change, no active invalidation on writes, deployment of multiple cache layers with different expiry windows, and missing cache warming after deployments.
Mitigations: keep TTLs proportional to how frequently data changes, implement event-driven cache invalidation for high-consistency data, use versioned cache keys (append a data version or ETag to the cache key) for strong consistency requirements, and monitor cache hit ratios to detect when stale data inflation indicates a configuration issue.
What metrics should a senior engineer monitor for a production caching layer?
Key caching metrics to monitor in production:
- Cache hit ratio โ a ratio below 80โ85% on a mature system may indicate over-expiration, bad key design, or data volatility that caching cannot effectively address.
- Eviction rate โ high eviction rates in
IMemoryCachesuggest undersized cache or priority misconfiguration. - Memory usage โ track Redis memory usage and fragmentation ratio; high fragmentation can cause unexpected OOM on Redis even when used memory appears within limits.
- Cache miss latency โ the p99 latency on cache miss paths is the cost of cache unavailability. Alert when miss latency exceeds acceptable thresholds.
- Redis connection pool health โ monitor for connection timeouts and command latencies using
StackExchange.Redisinstrumentation or OpenTelemetry. - Expired key rate โ a high rate of TTL-expired keys relative to evicted keys is healthy; a high rate of key evictions relative to expirations under memory pressure indicates an undersized cache.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
For a deeper dive into how ASP.NET Core manages caching internally and how to implement production-grade caching patterns, visit Coding Droplets or check the official Microsoft ASP.NET Core caching documentation for authoritative references and the latest framework updates.




