DEV Community

Cover image for ASP.NET Core Caching Explained: In-Memory, Redis, and Response Caching for High-Performance APIs
alinabi19
alinabi19

Posted on

ASP.NET Core Caching Explained: In-Memory, Redis, and Response Caching for High-Performance APIs

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
Enter fullscreen mode Exit fullscreen mode

With caching:

10,000 requests/min
→ Only 200 database queries
→ Remaining requests served from cache
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Expiration Strategies

Absolute Expiration

Cache expires after a fixed time.

AbsoluteExpirationRelativeToNow = 10 minutes
Enter fullscreen mode Exit fullscreen mode

Sliding Expiration

Expiration resets every time the cache is accessed.

SlidingExpiration = 2 minutes
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

This ensures fresh data is fetched on the next request.

Redis Distributed Cache Example

First install the package:

Microsoft.Extensions.Caching.StackExchangeRedis
Enter fullscreen mode Exit fullscreen mode

Configure Redis

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "MyApiCache";
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now all API instances share the same cache.

Response Caching Example

First enable response caching.

Program.cs

builder.Services.AddResponseCaching();
Enter fullscreen mode Exit fullscreen mode
app.UseResponseCaching();
Enter fullscreen mode Exit fullscreen mode

Controller Example

[HttpGet("catalog")]
[ResponseCache(Duration = 60)]
public IActionResult GetCatalog()
{
    var data = new
    {
        Message = "Cached Response",
        Time = DateTime.UtcNow
    };

    return Ok(data);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)