In-Memory Caching in ASP.NET Core Tutorial | IMemoryCache Example
Introduction
Performance is not an afterthought in backend systems—it’s a design responsibility.
One of the simplest yet most effective ways to improve API performance in .NET applications is In-Memory Caching. While often treated as a basic optimization, when used correctly, it becomes a foundational technique for reducing latency and improving system efficiency.
This article breaks down how in-memory caching works in .NET, when to use it, and the trade-offs you need to consider in real-world systems.
What is In-Memory Caching?
In-memory caching stores frequently accessed data directly in the application’s memory (RAM), allowing subsequent requests to retrieve data instantly without hitting the database or external services.
Instead of this flow:
Client → API → Database → Response
You get:
Client → API → Cache → Response
This significantly reduces response time and backend load.
Why It Matters
At scale, even small inefficiencies become expensive.
Repeated database calls for the same data lead to:
- Increased latency
- Higher infrastructure cost
- Reduced system throughput
Caching addresses these problems by eliminating redundant work.
How It Works in .NET
.NET provides built-in support through IMemoryCache, which is simple to integrate and highly efficient for single-instance applications.
Basic Registration
builder.Services.AddMemoryCache();
Simple Usage Pattern
Most developers use caching.
Fewer understand how to use it correctly in real APIs.
Here’s a simple In-Memory Caching implementation in .NET, but with production thinking applied
Controller Setup
private readonly IMemoryCache _cache; // Central cache interface (stored in server RAM)
public ProductController(IMemoryCache cache)
{
_cache = cache; // Injected via DI → ensures single cache instance per app
}
Data Source (Mock)
//List of Products
private static List<string> products = new List<string>
{
"TV",
"Laptop",
"Desktop Computer",
"Tablet",
"Smartphone",
"Smartwatch",
"Headphones",
"Earbuds",
"Bluetooth Speaker",
"Printer"
};
GET API with Caching
[HttpGet("{id}")]
public IActionResult GetProductById(int id)
{
if (id < 0 || id >= products.Count)
return NotFound("User not found");
// // Unique cache key → critical to avoid collisions
string cacheKey = $"productcachekey_{id}";
if (!_cache.TryGetValue(cacheKey, out string result))
{
// Simulating DB fetch → expensive operation in real-world
result = products[id].ToString();
// Cache miss → data not present in memory
var options = new MemoryCacheEntryOptions()
// Hard expiry → guarantees data freshness after 20s
.SetAbsoluteExpiration(TimeSpan.FromSeconds(20))
// Extends life if frequently accessed → keeps hot data alive
.SetSlidingExpiration(TimeSpan.FromSeconds(10));
// Store in cache with defined lifecycle
_cache.Set(cacheKey, result, options);
// First request → always from source
return Ok($"ID: {id} → Fresh: {result}");
}
// Subsequent requests → served from RAM (fast path)
return Ok($"ID: {id} → From Cache: {result}");
}
DELETE API (Cache Invalidation)
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
// Removing from source → data change
products.RemoveAt(id);
// Critical step → prevents stale data in cache
_cache.Remove($"productcachekey_{id}");
return Ok();
}
What Most Developers Miss
- Caching without expiration → stale data risk
- Caching without invalidation → inconsistent responses
- Poor cache key design → data conflicts
- Assuming cache is shared across servers → incorrect in scaling scenarios
What This Example Actually Demonstrates
- Cache-aside pattern (manual control over caching)
- Combination of absolute + sliding expiration
- Importance of cache invalidation on write operations
- Clear separation between source of truth vs performance layer
Limitations in Real Systems
In-memory caching works per application instance.
In a multi-server setup:
- Each server has its own cache
- Data becomes inconsistent across instances
This leads to:
- Cache duplication
- Unpredictable responses
Solution
Move to distributed caching (e.g., Redis) when:
- You scale horizontally
- You need shared cache across instances
Best Practices
- Cache only what is necessary
- Always define expiration policies
- Use meaningful cache keys
- Monitor memory usage
- Avoid caching large objects blindly
- Never treat cache as the source of truth
Final Thoughts
In-memory caching is not just a performance trick—it’s a design decision.
Used correctly, it improves system responsiveness and scalability with minimal effort. Used carelessly, it introduces inconsistency and hidden bugs.
Tech Lead Takeaway
Caching is not about making things faster.
It’s about removing unnecessary work from your system.
This example is simple—but the thinking behind it is what scales.
dotnet #webapi #csharp #caching #softwarearchitecture #backendengineering #systemdesign #performance #techleadership