Modern APIs rarely fail because of logic.
They fail because of performance.
Picture this.
You deploy a clean ASP.NET Core API. Everything works perfectly during testing. But once real traffic arrives, things start getting ugly:
- Database CPU spikes
- Queries that took 30 ms now take 600 ms
- Your API latency slowly creeps past 1 second
The problem usually isn’t the database itself.
It’s that your API is doing the same expensive work repeatedly.
The same product list.
The same configuration settings.
The same reference data.
Over and over again.
This is where caching becomes one of the most powerful tools in backend engineering.
Yet many developers either:
- Avoid caching because it feels complicated
- Or implement it incorrectly and create stale data bugs
In this guide, you'll learn how caching actually works in ASP.NET Core, when to use each type, and how to implement it properly in production APIs.
By the end, you'll know how to:
- Improve API performance dramatically
- Reduce database load
- Build APIs that scale without infrastructure explosions
Let’s start with the fundamentals.
Why API Performance Matters
API performance directly impacts:
- User experience
- Infrastructure cost
- Scalability
- System stability
Consider this example:
Without caching:
10,000 requests/min
→ Each request hits the database
→ 10,000 database queries/min
With caching:
10,000 requests/min
→ Only 200 database queries
→ Remaining requests served from cache
The result:
- Faster responses
- Lower DB load
- Better scalability
Caching is often the highest ROI optimization you can implement.
What Is Caching
Caching means storing expensive data temporarily so it can be reused.
Instead of recomputing or querying the database repeatedly, the API retrieves the result from a fast storage layer.
Typical cache flow:
Request arrives
↓
Check Cache
↓
Cache Hit → return data immediately
Cache Miss → fetch from DB → store in cache → return
The key idea:
Cache the result of expensive operations, not everything.
Types of Caching in ASP.NET Core
ASP.NET Core provides several caching mechanisms:
1. In-Memory Caching
2. Distributed Caching
3. Response Caching
Each solves a different problem.
In-Memory Caching
In-memory caching stores data inside the application server's memory.
It is:
- Extremely fast
- Easy to implement
- Best for single-instance APIs
However, it has a limitation.
If you run multiple API instances (load balancing), each instance has its own separate cache.
Use cases:
- Product catalogs
- Configuration values
- Reference data
Distributed Caching (Redis / SQL Server)
Distributed caching stores data in external cache systems like:
- Redis
- SQL Server
- NCache
All application instances share the same cache.
Benefits:
- Works with multiple API instances
- Ideal for cloud and microservices architectures
- Handles large-scale traffic
This is the standard choice for production systems.
Response Caching
Response caching stores entire HTTP responses.
Instead of executing the controller again, ASP.NET Core returns the cached HTTP response.
This is useful for:
- Public APIs
- GET endpoints
- Static data endpoints
However, it only works when responses are safe to cache.
When to Use Each Type
| Cache Type | Best Use Case | Speed | Scalability |
|---|---|---|---|
| In-Memory Cache | Single instance apps | Very fast | Low |
| Distributed Cache | Multi-server production APIs | Fast | High |
| Response Cache | Cache entire HTTP responses | Very fast | Medium |
Rule of thumb:
- Small apps → In-Memory
- Scalable APIs → Redis Distributed Cache
- Static GET responses → Response Caching
Step-by-Step Implementation
Let's implement each caching strategy.
In-Memory Cache Example
First, register the memory cache service.
Program.cs (.NET 8)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Using IMemoryCache
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly IMemoryCache _cache;
public ProductsController(IMemoryCache cache)
{
_cache = cache;
}
[HttpGet]
public async Task<IActionResult> GetProducts()
{
const string cacheKey = "product_list";
if (!_cache.TryGetValue(cacheKey, out List<string> products))
{
// Simulate expensive DB call
await Task.Delay(500);
products = new List<string>
{
"Laptop",
"Keyboard",
"Mouse"
};
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
SlidingExpiration = TimeSpan.FromMinutes(2)
};
_cache.Set(cacheKey, products, cacheOptions);
}
return Ok(products);
}
}
Expiration Strategies
Absolute Expiration
Cache expires after a fixed time.
AbsoluteExpirationRelativeToNow = 10 minutes
Sliding Expiration
Expiration resets every time the cache is accessed.
SlidingExpiration = 2 minutes
Combining both prevents stale data.
Cache Invalidation Example
Cache invalidation is crucial when data changes.
[HttpPost]
public IActionResult AddProduct(string name)
{
// Save to database
_cache.Remove("product_list");
return Ok();
}
This ensures fresh data is fetched on the next request.
Redis Distributed Cache Example
First install the package:
Microsoft.Extensions.Caching.StackExchangeRedis
Configure Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApiCache";
});
Using IDistributedCache
public class ProductsController : ControllerBase
{
private readonly IDistributedCache _cache;
public ProductsController(IDistributedCache cache)
{
_cache = cache;
}
[HttpGet("redis")]
public async Task<IActionResult> GetProductsRedis()
{
var cacheKey = "products";
var cachedData = await _cache.GetStringAsync(cacheKey);
if (cachedData != null)
{
return Ok(JsonSerializer.Deserialize<List<string>>(cachedData));
}
var products = new List<string>
{
"Laptop",
"Keyboard",
"Mouse"
};
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
};
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(products),
options
);
return Ok(products);
}
}
Now all API instances share the same cache.
Response Caching Example
First enable response caching.
Program.cs
builder.Services.AddResponseCaching();
app.UseResponseCaching();
Controller Example
[HttpGet("catalog")]
[ResponseCache(Duration = 60)]
public IActionResult GetCatalog()
{
var data = new
{
Message = "Cached Response",
Time = DateTime.UtcNow
};
return Ok(data);
}
Now responses are cached for 60 seconds.
Production Insight: Cache Stampede
A common issue in high-traffic systems is cache stampede.
When a cache entry expires, many concurrent requests may attempt to recompute the same value simultaneously.
This can overwhelm your database.
Typical mitigation strategies include:
- Using SemaphoreSlim locking
- Using
Lazy<T>caching - Refreshing cache in the background
- Using stale-while-revalidate patterns
Without protection, a popular endpoint can trigger thousands of database queries the moment a cache expires.
Real-World Use Cases
Caching shines in scenarios like:
Product catalog APIs
Product data changes rarely but is requested constantly.
Configuration endpoints
Feature flags, settings, metadata.
Dashboard APIs
Expensive aggregations or analytics queries.
Reference data
Countries, currencies, categories.
Common Mistakes Developers Make
Caching everything
Not all data should be cached. Only cache expensive operations.
Forgetting cache invalidation
Stale data bugs happen when cache isn't cleared after updates.
Using in-memory cache in scaled systems
Multiple servers = multiple caches = inconsistent data.
Using very long expiration times
Leads to stale responses.
Performance & Scalability Considerations
When designing caching strategies:
Think about:
- Cache eviction policies
- Memory consumption
- Cache hit ratio
- Invalidation strategy
Monitor:
- Cache hits vs misses
- Redis latency
- Database query reductions
A good cache system should dramatically reduce database load.
Best Practices for Production APIs
Follow these principles:
- Cache read-heavy endpoints
- Use Redis for distributed systems
- Always define expiration policies
- Implement cache invalidation
- Monitor cache performance
Pro Tip
Cache DTO results, not raw entities.
This avoids serialization overhead and prevents accidental cache mutation.
Common Pitfall
Never cache user-specific data globally.
Example:
GET /api/orders
If cached improperly, users might receive other users’ data.
Always include user context in cache keys when needed.
Why Caching Is a Game Changer for APIs
Caching is one of the simplest ways to improve API performance.
A well-designed caching layer can:
- Reduce database load dramatically
- Improve API latency
- Increase scalability
And the best part?
You often get 10x performance improvements with minimal code changes.
Start simple:
- Add in-memory caching
- Move to Redis when scaling
- Use response caching for public endpoints
Small optimizations like these are what separate average APIs from high-performance systems.
Quick Recap
- Caching reduces repeated expensive operations
- ASP.NET Core supports multiple caching strategies
- Redis enables scalable distributed caching
- Cache invalidation is critical
- Always implement expiration policies
One Question for You
What caching strategy are you currently using
in your ASP.NET Core APIs?
In-memory? Redis? Something else?
I'd love to hear your experience.
Top comments (0)