Understanding Cache Stampede and How HybridCache Solves It in ASP.NET Core
Introduction
Imagine you're developing an e-commerce application using ASP.NET Core Web API, Entity Framework Core, SQL Server, and Redis.
Your product details endpoint is performing well.
You've implemented caching to reduce unnecessary database queries.
Everything looks good until your application receives a sudden spike in traffic.
At that exact moment, a popular product's cache entry expires.
Now 100 users request the same product simultaneously.
What happens next?
All 100 requests check the cache.
All 100 requests discover that the cached entry is missing.
And without proper coordination, all 100 requests execute the same SQL query.
Your database suddenly processes 100 identical queries to retrieve information that only needs to be fetched once.
This problem is known as a Cache Stampede, sometimes called the Thundering Herd Problem.
Let's understand why it happens, how it affects application performance, and how Microsoft's HybridCache can help solve it.
1. What Is a Cache Stampede?
A cache stampede occurs when multiple concurrent requests attempt to regenerate the same missing or expired cached data.
Consider an API endpoint:
GET /api/products/101
The product details are stored in Redis with a 10-minute expiration.
While the entry exists, requests are served from Redis.
But when the cache entry expires, multiple requests may reach the database simultaneously.
Without Cache Stampede Protection
100 Concurrent Requests
|
v
Redis Cache
|
CACHE MISS
|
+-----------+-----------+
| | |
Request 1 Request 2 Request 100
| | |
v v v
SQL Query SQL Query SQL Query
| | |
+-----------+-----------+
|
v
SQL Server
100 requests = up to 100 queries
Every request independently tries to repopulate the cache.
This creates unnecessary database work.
Why Is This Dangerous?
A cache stampede can cause:
- Sudden spikes in database connections
- Increased SQL Server CPU utilization
- Higher API response times
- Database connection pool pressure
- Request timeouts
- Reduced application throughput
In extreme cases, a database slowdown can trigger additional request retries, increasing pressure on an already overloaded system.
The important point is that caching alone doesn't automatically prevent a cache stampede.
2. Traditional Cache-Aside Implementation
Let's look at a common implementation using IDistributedCache.
public async Task<Product?> GetProductAsync(
int productId,
CancellationToken cancellationToken)
{
string cacheKey = $"product:{productId}";
var cachedProduct = await _cache.GetStringAsync(
cacheKey,
cancellationToken);
if (cachedProduct != null)
{
return JsonSerializer.Deserialize<Product>(
cachedProduct);
}
var product = await _dbContext.Products
.AsNoTracking()
.FirstOrDefaultAsync(
p => p.Id == productId,
cancellationToken);
if (product != null)
{
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(10)
},
cancellationToken);
}
return product;
}
This implementation works correctly for ordinary cache hits and misses.
However, it doesn't coordinate concurrent cache misses.
If 100 requests check Redis before any request has populated the entry, they can all proceed to SQL Server.
The cache is eventually populated, but the database has already performed duplicate work.
This is the limitation we want to address.
3. The Solution: HybridCache in ASP.NET Core
Microsoft introduced HybridCache to simplify caching scenarios that need more than basic cache reads and writes.
HybridCache provides:
- A primary in-memory cache
- An optional secondary distributed cache
- Stampede protection for concurrent requests
- Cache entry expiration
- Tag-based invalidation
- Asynchronous APIs
The most interesting feature for our scenario is Stampede Protection.
When multiple callers request the same missing cache key within one application instance, HybridCache can coordinate them so that the underlying data factory executes once.
Other callers await the shared operation instead of independently querying the database.
Simple Analogy
Imagine 100 people standing in front of a lift.
All 100 people want to go to the same floor.
Without coordination, imagine everyone requesting a separate lift.
With coordination, one lift arrives and the group shares the ride.
HybridCache applies a similar idea to concurrent cache misses.
4. Implement HybridCache in ASP.NET Core
Let's implement HybridCache using ASP.NET Core Web API and Entity Framework Core.
Step 1: Install HybridCache
dotnet add package Microsoft.Extensions.Caching.Hybrid
Step 2: Register HybridCache
In Program.cs:
builder.Services.AddHybridCache();
This registers the HybridCache services.
For a basic implementation, you can start with the in-memory caching layer.
Step 3: Implement the Product Service
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.EntityFrameworkCore;
public class ProductService
{
private readonly HybridCache _cache;
private readonly AppDbContext _dbContext;
public ProductService(
HybridCache cache,
AppDbContext dbContext)
{
_cache = cache;
_dbContext = dbContext;
}
public async Task<ProductDto?> GetProductAsync(
int productId,
CancellationToken cancellationToken)
{
string cacheKey = $"product:{productId}";
return await _cache.GetOrCreateAsync(
cacheKey,
async ct =>
{
return await _dbContext.Products
.AsNoTracking()
.Where(p => p.Id == productId)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.FirstOrDefaultAsync(ct);
},
cancellationToken: cancellationToken);
}
}
What Happens Internally?
Suppose 100 requests arrive for product 101 after its cache entry expires.
First request:
- Checks HybridCache.
- Cache entry is missing.
- Executes the EF Core query.
- Stores the result in the cache.
- Returns the product.
Other concurrent requests:
- Check the same cache key.
- Find that the cache population operation is already running.
- Await the shared operation.
- Receive its result without independently executing the same query.
For overlapping misses on the same key within the same application instance, the factory is coordinated.
100 concurrent requests can therefore result in one database query instead of 100.
This assumes the requests share the same HybridCache instance and the operation is eligible for the normal coordinated cache path.
5. How HybridCache Works with Redis
HybridCache supports two caching levels.
L1 Cache — In-Memory Cache
Stores frequently accessed data inside the application's process.
This avoids network calls for local cache hits.
L2 Cache — Distributed Cache
Uses a distributed caching provider such as Redis.
This allows multiple application instances to access shared cached data.
Install Redis Integration
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Configure Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration.GetConnectionString("Redis");
});
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions =
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
});
appsettings.json
{
"ConnectionStrings": {
"Redis": "localhost:6379"
}
}

Join the conversation! Your thoughts help the community grow.