← Back to Article List         

In-Memory Caching in ASP.NET Core Tutorial | IMemoryCache Example

Published on 16 Apr 2026     4 min read C#
C Sharp Caching in .NET

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