aspnet-core-caching-rendimiento-redis-memory

Caching in ASP.NET Core: IMemoryCache, Redis and HybridCache

  • 5 min

An idea[cache] is a temporary storage of data that avoids repeating expensive work, such as querying the same information from the database for every request.

Let’s consider an endpoint GET /api/productos that returns a store’s catalog:

  1. The API receives the request.
  2. It opens a connection to SQL Server.
  3. It executes a complex query.
  4. It serializes the data.
  5. It returns the response.

This takes, say, 200ms. It doesn’t seem like much. But if you have 1000 users hitting F5 at the same time, your database will become saturated and the website will crash. Besides, why query the database a thousand times if the product catalog changes once a week?

The solution is Caching: store the result in RAM (which is thousands of times faster than disk/SQL) and serve it from there.

In-Memory Cache

The most basic form is to use the RAM of the server where your API runs. ASP.NET Core provides this natively.

Configuration

In Program.cs, we simply enable the service:

builder.Services.AddMemoryCache();
Copied!

Usage in the Service

Let’s modify a ProductoService to try reading from the cache before bothering the database.

using Microsoft.Extensions.Caching.Memory;

public class ProductoService
{
    private readonly ApplicationDbContext _db;
    private readonly IMemoryCache _cache; // 👈 We inject this

    public ProductoService(ApplicationDbContext db, IMemoryCache cache)
    {
        _db = db;
        _cache = cache;
    }

    public async Task<List<Producto>> GetAll()
    {
        const string cacheKey = "lista_productos";

        // 1. Check: Is it in the cache?
        if (!_cache.TryGetValue(cacheKey, out List<Producto>? productos))
        {
            // 2. CACHE MISS (Not there): Go to database
            productos = await _db.Productos.ToListAsync();

            // 3. Configure options (When it expires)
            var cacheOptions = new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(10)) // Expires in 10 min
                .SetSlidingExpiration(TimeSpan.FromMinutes(2));  // If unused for 2 min, remove

            // 4. Store in cache
            _cache.Set(cacheKey, productos, cacheOptions);
        }

        // 5. CACHE HIT: Return the data (fast)
        return productos ?? [];
    }
}
Copied!

The limitation of local memory

IMemoryCache is very useful, but each process maintains its own copy. If your API is scaled across 3 servers (containers) behind a load balancer:

  • A user makes a request and hits Server A. It is saved in server A’s cache.
  • The user makes another request and hits Server B. Server B knows nothing about server A’s cache! It queries the database again.

To solve this, we need a Distributed Cache.

Distributed Cache with Redis

Redis is an ultra-fast in-memory database that works as an independent server. All your API instances (Server A, B, and C) connect to the same Redis. If A saves something, B can read it.

Installation

We need a package to communicate with Redis.

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Copied!

You also need a Redis server. For local development, you can start one with Docker:

docker run -d -p 6379:6379 --name mi-redis redis:latest
Copied!

This works for local development. In production, Redis should have authentication, private network, backups if applicable, and a clear memory policy.

Configuration

In Program.cs, replace AddMemoryCache with this:

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379"; // Your connection string
    options.InstanceName = "MiApi_"; // Prefix for keys
});
Copied!

Using IDistributedCache

The interface changes slightly (IDistributedCache). The big difference is that Redis does not store C# objects, it stores bytes. Therefore, you need to serialize to JSON before saving and deserialize when reading.

using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

public class ProductoService
{
    private readonly IDistributedCache _cache; // 👈 Distributed interface

    public async Task<List<Producto>> GetAll()
    {
        string key = "lista_productos";

        // 1. Try to get a string from the cache
        string? cachedJson = await _cache.GetStringAsync(key);

        if (!string.IsNullOrEmpty(cachedJson))
        {
            // CACHE HIT: Deserialize and return
            return JsonSerializer.Deserialize<List<Producto>>(cachedJson) ?? [];
        }

        // 2. CACHE MISS: Query the database
        var productos = await _db.Productos.ToListAsync();

        // 3. Save to cache (Serializing)
        var options = new DistributedCacheEntryOptions
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10));

        string jsonToSave = JsonSerializer.Serialize(productos);
        await _cache.SetStringAsync(key, jsonToSave, options);

        return productos;
    }
}
Copied!

Expiration Strategies

A cache cannot be eternal (memory is expensive and data changes). You have two ways to decide when to delete data:

  1. Absolute Expiration: “This data lives for 10 minutes. No matter what, after 10 minutes it dies.”
  • Usage: Data that needs periodic refresh (e.g., Prices, Stock).
  1. Sliding Expiration: “This data lives for 10 minutes. BUT if someone reads it at minute 9, I reset the counter to another 10 minutes.”
  • Usage: User sessions. As long as you keep navigating, you won’t be kicked out.

Stale Data and Invalidation

Introducing a cache introduces a new problem: Inconsistency. If you change a product’s price in the database, the cache will show the old price for 10 minutes until it expires.

We can solve this with cache invalidation: when we modify data, we delete its entry so the next read reloads it.

public async Task ActualizarProducto(Producto p)
{
    _db.Productos.Update(p);
    await _db.SaveChangesAsync();

    // 💣 IMPORTANT! Delete the old cache to force a refresh
    await _cache.RemoveAsync("lista_productos");
}
Copied!

If many requests cause the same cache miss simultaneously, they could all query the database. HybridCache, available in modern versions of .NET, combines local and distributed cache and adds protection against this problem (cache stampede).