Caching Strategies Explained
Choosing the Right One for Your System
Think of caching like a kitchen prep station. A good chef doesn’t fetch every ingredient from the pantry for every order. They prep the most-used ingredients and keep them within arm’s reach. But prep too much, and the food goes stale. Prep the wrong things, and you’re still running back and forth to the pantry anyway.
Caching in software works the same way. The right strategy keeps your system fast and your database healthy. The wrong one gives you stale data, wasted memory, or a database that gets crushed the moment a cache key expires.
In this post, we’ll walk through the six major caching strategies, when each one shines, and where each one falls apart. We’ll also cover the common pitfalls that catch teams off guard and how .NET’s caching stack has evolved to address them.
Let’s get started!
Cache-Aside (Lazy Loading)
This is the strategy most developers learn first, and for good reason. It’s simple, and it works.
How it works:
1. Application receives a read request
2. Checks the cache for the data
3. On a hit, returns data from cache
4. On a miss, queries the database, writes the result to cache, then returns it
For writes, the application writes directly to the database and invalidates the cache entry. The next read will repopulate it.
Pros:
Simple to implement and reason about
Resilient to cache failures (falls back to the database)
Only requested data is cached (no wasted memory)
Cons:
First request for any key always hits the database (cold start)
Risk of stale data if invalidation is missed
Every service accessing the data must implement the pattern correctly
Best for: General-purpose read-heavy workloads. E-commerce product catalogues, user profiles, and content management systems.
Cache-aside is the safe default. If you’re unsure which strategy to use, start here.
Read-Through
Read-through looks similar to cache-aside, but there’s an important difference: the cache itself is responsible for fetching data on a miss, not the application.
How it works:
1. Application requests data from the cache
2. On a hit, the cache returns the data
3. On a miss, the cache fetches from the database, stores the result, and returns it
The application never talks to the database directly for reads. It only talks to the cache.
Pros:
Cleaner application code (no cache-miss logic scattered everywhere)
Enforces separation of concerns
Cons:
The cache provider must know how to query your database (tighter coupling)
More complex setup and configuration
Best for: Read-heavy workloads with predictable access patterns. News feeds, product listings, reference data.
Read-through is often paired with write-through or write-behind for a complete caching solution.
Write-Through
Write-through is the strategy you reach for when consistency matters more than write speed.
How it works:
1. Application writes data to the cache
2. The cache synchronously writes the same data to the database
3. Both writes must succeed before the caller gets an acknowledgement
Because every write goes through the cache first, reads are always up to date. There’s no stale data window.
Pros:
Strong consistency between cache and database
Reads are always fast (cache is always warm for recently written data)
Simple mental model for data freshness
Cons:
Higher write latency (you’re waiting for two writes on every operation)
Write-heavy workloads take a significant performance hit
Data that’s written but rarely read still occupies cache memory
Best for: Financial systems, inventory management, and user sessions. Anywhere the cost of serving stale data exceeds the cost of slower writes.
Write-Behind (Write-Back)
Write-behind flips the consistency tradeoff. It prioritises write speed and accepts eventual consistency.
How it works:
1. Application writes data to the cache
2. Cache immediately acknowledges the write
3. In the background, the cache batches and flushes writes to the database asynchronously
The application never waits for the database write to complete. This gives you the lowest write latency of any strategy.
Pros:
Extremely low write latency
Batching reduces database load (10 individual writes become 1 batch insert)
Excellent for write-heavy workloads
Cons:
If the cache node crashes before flushing, that data is lost
Eventual consistency between cache and database
Debugging is harder (database state lags behind cache state)
Best for: Analytics event ingestion, social media activity feeds (likes, views, impressions), IoT sensor data, logging systems.
I need to emphasise: only use write-behind when you can tolerate some data loss, or when you have cache replication in place as a safety net.
Write-Around
Write-around is the quiet one. It doesn’t get talked about much, but it solves a specific and common problem: cache pollution.
How it works:
1. Application writes data directly to the database, bypassing the cache entirely
2. The cache is not updated on writes
3. Reads follow cache-aside or read-through; data enters the cache only when it’s actually requested
Pros:
Prevents the cache from filling up with data that’s written once and never read
Cache memory is reserved for frequently accessed data
Simple write path
Cons:
First read after a write always hits the database
Not suitable if writes are immediately followed by reads
Best for: Log ingestion, audit trails, batch data imports, and real-time chat message storage. Any workload where you write far more data than you read.
Refresh-Ahead
Refresh-ahead is the proactive strategy. Instead of waiting for a cache entry to expire and then suffering a miss, it refreshes entries before they expire.
How it works:
1. Application reads from cache as normal
2. When an entry is within a configurable window before expiration (say, 80% through its TTL), the cache returns the current value immediately and triggers an asynchronous background refresh
3. The background job fetches fresh data and updates the cache before the TTL expires
Pros:
Eliminates latency spikes for popular keys
Predictable, consistent response times
Users always hit a warm cache for hot data
Cons:
Wasted refreshes for entries that nobody accesses again
Increased backend load from proactive fetches
Requires predictable access patterns to be cost-effective
Best for: Dashboards, leaderboards, stock tickers, and news homepage data. Anywhere you have high-traffic keys that must always be fast.
Combining Strategies
In practice, most production systems don’t use a single strategy. They combine them:
Cache-Aside + Write-Around: the most common pairing. Reads are cached on demand. Writes bypass the cache. Simple and effective for most CRUD applications.
Read-Through + Write-Through: full cache mediation. The application never touches the database directly. Strong consistency with clean code.
Read-Through + Write-Behind: high-throughput systems that need both fast reads and fast writes, and can tolerate eventual consistency.
Cache-Aside + Refresh-Ahead: for the critical hot paths. Most data uses regular cache-aside. The top 1% of keys get a proactive refresh.
The right combination depends on your consistency requirements, read/write ratio, and tolerance for complexity.
The Pitfalls Nobody Warns You About
Cache Stampede (Thundering Herd)
A popular cache key expires. Hundreds of concurrent requests see the miss, and all hit the database simultaneously.
This is exactly what caused one of Facebook’s biggest outages in 2010. A configuration change invalidated a frequently-accessed cache entry. The resulting stampede overwhelmed their database, and error-handling logic made it worse by deleting more cache keys, creating a self-reinforcing cascade that lasted 2.5 hours.
Solutions: distributed locking (only one request fetches, others wait), probabilistic early expiration (random chance of refreshing before TTL), or use .NET’s HybridCache, which handles this automatically with built-in stampede protection.
Cache Avalanche
Many keys expire at the same time. This typically happens when you set the same TTL on everything at startup.
Solution: Add random jitter to your TTLs. Instead of 10 minutes for every key, use 10 minutes + a random 0-60 seconds.
Cache Penetration
Requests for keys that don’t exist in the cache or the database. Every request passes through to the database. Often caused by bots or bugs.
Solution: Cache null results with a short TTL. For heavy traffic, use a Bloom filter to quickly reject keys that are known not to exist.
.NET’s Caching Stack in 2026
The .NET caching story has matured significantly:
IMemoryCache: in-process, single-server. Fast, but lost on restart and not shared across instances.
IDistributedCache: abstraction over Redis, SQL Server, and CosmosDB. Shared across instances but requires serialisation.
HybridCache (.NET 9+): the new recommended default. Combines L1 (in-process) + L2 (distributed) with stampede protection and tag-based invalidation out of the box.
HybridCache deserves special attention. It eliminates the hand-rolled cache-aside boilerplate that’s in every .NET codebase:
// Before: manual cache-aside
var product = await cache.GetAsync<Product>($”product-{id}”);
if (product is null)
{
product = await db.Products.FindAsync(id);
await cache.SetAsync($”product-{id}”, product,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
}
// After: HybridCache
var product = await cache.GetOrCreateAsync(
$”product-{id}”,
async token => await db.Products.FindAsync(id, token),
tags: [”products”, $”category-{categoryId}”]
);
// Bulk invalidation by tag
await cache.RemoveByTagAsync($”category-{categoryId}”);If 100 requests arrive for the same missing key simultaneously, HybridCache runs the factory method once and returns the same result to all 100 requests. No stampede. No duplicate database calls.
Conclusion
Caching is not a single tool. It’s a toolkit. Cache-aside is the safe starting point, but knowing when to reach for write-through, write-behind, or refresh-ahead is what separates systems that scale from systems that buckle under load.
Like everything in software engineering, one needs to weigh the pros and cons. Strong consistency costs write latency. Low write latency risks data loss. Proactive refresh costs backend resources. There’s no free lunch.
My advice: start with cache-aside. Add complexity only when you have evidence that it’s needed. Always set TTLs as a safety net. And if you’re on .NET 9+, make HybridCache your default; it handles the hardest problems (stampede protection, L1+L2, tag invalidation) so you don’t have to.

