ASP.NET Core  

Implementing Caching Strategies (Redis + MemoryCache) for High-Performance APIs in ASP.NET Core

1. Introduction

As API traffic scales, even optimized SQL queries can cause latency and performance bottlenecks under heavy load. Caching is one of the most effective strategies to reduce database hits and improve response time.

ASP.NET Core offers two primary caching mechanisms:

  • In-Memory Caching (MemoryCache): Keeps data within the application memory.

  • Distributed Caching (Redis): Stores data centrally, accessible across multiple servers.

By combining these two caching approaches, we can build a hybrid caching strategy that leverages both speed and scalability.

2. Why Caching Matters in APIs

When implemented properly, caching can:

  • Decrease response time by returning frequently requested data from memory instead of the database.

  • Reduce database load, preventing query contention and bottlenecks.

  • Improve scalability, allowing the system to handle high traffic more efficiently.

  • Minimize external API costs, when third-party API results are cached.

3. Common Caching Layers

Caching TypeDescriptionScope
MemoryCacheStores data in the application's local memory.Single server
Redis CacheDistributed cache accessible across servers or containers.Multi-server
Response CachingCaches entire HTTP responses.Web Layer
EF Core Second-Level CacheCaches EF Core query results.Data Layer

4. Two-Tier Caching Strategy (MemoryCache + Redis)

In a hybrid approach:

  1. MemoryCache is used for frequently accessed ("hot") data.

  2. Redis is used as a shared cache between multiple API servers.

When a request comes in:

  • The system checks MemoryCache first.

  • If not found, it checks Redis.

  • If not found in Redis either, it fetches data from the database, caches it in both layers, and returns it.

5. Technical Workflow (Flowchart)

        ┌───────────────────────────────┐
        │           API Request         │
        └──────────────┬────────────────┘
                       │
                       ▼
          ┌────────────────────────┐
          │ Check MemoryCache       │
          └──────────────┬─────────┘
                         │
          ┌──────────────┴─────────────┐
          │ Found? → Return from cache │
          └──────────────┬─────────────┘
                         │
                         ▼
          ┌────────────────────────┐
          │ Check Redis Cache       │
          └──────────────┬─────────┘
                         │
          ┌──────────────┴─────────────┐
          │ Found? → Store in MemoryCache│
          │ and Return Response          │
          └──────────────┬─────────────┘
                         │
                         ▼
          ┌────────────────────────┐
          │ Fetch from Database     │
          └──────────────┬─────────┘
                         │
                         ▼
          ┌────────────────────────┐
          │ Store in Redis + Memory │
          │ Return Response         │
          └────────────────────────┘

6. Implementation in ASP.NET Core

Let’s go step by step to implement caching using both Redis and MemoryCache.

Step 1: Install Required Packages

Install the following NuGet packages:

dotnet add package Microsoft.Extensions.Caching.Memory
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package StackExchange.Redis

Step 2: Configure Caching in Program.cs

var builder = WebApplication.CreateBuilder(args);

// Memory Cache
builder.Services.AddMemoryCache();

// Redis Cache
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379"; // Replace with your Redis connection string
    options.InstanceName = "MyApp_";
});

builder.Services.AddControllers();
var app = builder.Build();

app.MapControllers();
app.Run();

Step 3: Create a Cache Service Interface

public interface ICacheService
{
    Task<T> GetAsync<T>(string key);
    Task SetAsync<T>(string key, T value, TimeSpan duration);
    Task RemoveAsync(string key);
}

Step 4: Implement Hybrid Cache Service (Redis + MemoryCache)

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

public class HybridCacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;

    public HybridCacheService(IMemoryCache memoryCache, IDistributedCache distributedCache)
    {
        _memoryCache = memoryCache;
        _distributedCache = distributedCache;
    }

    public async Task<T> GetAsync<T>(string key)
    {
        // Step 1: Check MemoryCache
        if (_memoryCache.TryGetValue(key, out T value))
            return value;

        // Step 2: Check Redis
        var cachedData = await _distributedCache.GetStringAsync(key);
        if (!string.IsNullOrEmpty(cachedData))
        {
            value = JsonSerializer.Deserialize<T>(cachedData);

            // Store in MemoryCache for faster next access
            _memoryCache.Set(key, value, TimeSpan.FromMinutes(2));
            return value;
        }

        return default;
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan duration)
    {
        // Cache in both Memory and Redis
        _memoryCache.Set(key, value, duration);

        var data = JsonSerializer.Serialize(value);
        await _distributedCache.SetStringAsync(key, data, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = duration
        });
    }

    public async Task RemoveAsync(string key)
    {
        _memoryCache.Remove(key);
        await _distributedCache.RemoveAsync(key);
    }
}

Step 5: Register in Dependency Injection

builder.Services.AddScoped<ICacheService, HybridCacheService>();

Step 6: Use Cache in Controller

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ICacheService _cacheService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(ICacheService cacheService, ILogger<ProductsController> logger)
    {
        _cacheService = cacheService;
        _logger = logger;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        string cacheKey = $"product_{id}";

        // Step 1: Try to get data from cache
        var cachedProduct = await _cacheService.GetAsync<Product>(cacheKey);
        if (cachedProduct != null)
        {
            _logger.LogInformation("Data fetched from cache.");
            return Ok(cachedProduct);
        }

        // Step 2: Simulate database fetch
        var product = new Product
        {
            Id = id,
            Name = $"Product-{id}",
            Price = 1500 + id
        };

        // Step 3: Cache the data
        await _cacheService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(5));
        _logger.LogInformation("Data fetched from DB and cached.");

        return Ok(product);
    }
}

Sample Model

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

7. Cache Expiration Strategies

StrategyDescriptionExample
Absolute ExpirationRemoves an item after a fixed duration.TimeSpan.FromMinutes(5)
Sliding ExpirationResets expiration timer on each access.SlidingExpiration = TimeSpan.FromMinutes(2)
Manual EvictionRemoves item explicitly via key.RemoveAsync("product_1")

Example:

_memoryCache.Set("data_key", data, new MemoryCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(3)
});

8. Best Practices

  1. Use JSON serialization for Redis to maintain interoperability.

  2. Version cache keys (e.g., product_v2_123) to avoid invalid cache issues.

  3. Cache read-heavy data only; avoid caching frequently updated values.

  4. Invalidate cache on updates to keep data consistent.

  5. Set appropriate expiration times to avoid stale data.

  6. Monitor Redis memory and configure eviction policies properly.

9. Cache Invalidation Using Redis Pub/Sub

In distributed systems, when one server updates data, others must invalidate outdated cache entries.
Redis Pub/Sub makes this simple:

Publisher Example

await _redis.PublishAsync("invalidate_cache", "product_123");

Subscriber Example

_redis.Subscribe("invalidate_cache", (channel, message) =>
{
    _memoryCache.Remove(message);
});

This ensures cache consistency across multiple application nodes.

10. Performance Benchmark Example

ScenarioWithout CacheMemoryCache OnlyRedis + MemoryCache
Avg Response Time (ms)4209560
DB Calls per 1000 requests100020075
CPU Load80%50%35%

The hybrid approach improves performance by up to 7x and significantly reduces database calls.

11. Security and Reliability Considerations

  • Avoid caching sensitive data like passwords or tokens unless encrypted.

  • Enable Redis persistence (RDB or AOF) for recovery after restarts.

  • Use Redis authentication for security.

  • Monitor TTL values and implement eviction policies.

  • Pool Redis connections for high traffic environments.

12. CI/CD and Deployment Integration

In a CI/CD setup (Jenkins, GitHub Actions, or Azure DevOps):

  • Configure Redis connection strings as environment variables.

  • Add a cache clearing step after deployments to prevent stale data:

    redis-cli FLUSHALL
    
  • Implement health checks for Redis in ASP.NET Core:

    builder.Services.AddHealthChecks().AddRedis("localhost:6379", "Redis Health");
    

13. Common Pitfalls

IssueDescriptionFix
Cache miss on every requestSerialization or key mismatchEnsure consistent JSON serializer
MemoryCache growthMissing expiration policyAlways set TTL
Redis latencyHigh network overheadUse local Redis cluster or caching gateway
Data inconsistencyCache not invalidated after updatesImplement proper invalidation or TTL

14. Summary

Caching is one of the most critical optimizations for high-performance APIs.
The hybrid MemoryCache + Redis strategy offers both speed and scalability:

  • MemoryCache → ultra-fast lookups for local data

  • Redis → centralized caching for distributed systems

  • Combined → 7x faster API responses and lower infrastructure load

This approach ensures your ASP.NET Core APIs are production-ready, efficient, and scalable across environments.