The Quick Notes

Built with caffeine, curiosity, and a suspicious number of tabs

Hybrid Cache with In-Memory + Redis in .NET

Truc Nguyen

Keywords: tech, c#, dotnet, caching, redis

Abstract

A quick note on setting up hybrid caching in .NET — combining in-memory (L1) and Redis (L2) for fast, scalable APIs. Includes config, load test results, and trade-offs.

Hybrid cache = in-memory (L1) + Redis (L2). Reads hit L1 first (fast), then L2, then the data source. Redis keeps the cache consistent across multiple server instances.


How it works

client  L1 (memory)  L2 (redis)  database
          cache hit    cache hit   cache miss
  • L1 (in-memory): fastest. local to each server process.
  • L2 (redis): shared across all server instances. survives restarts.
  • database: only called on a full cache miss.

Setup

Register hybrid cache with Redis as the distributed backend:

builder.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(5),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };
});

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "hybrid-cache-demo";
});

Cache entries expire from both L1 and L2 after 5 minutes.

Sample endpoint

app.MapGet("/v1/categories", async (ILookupService service) =>
{
    var result = await service.GetCategoriesAsync();
    return Results.Ok(result);
});

Load test results

Using bombardier — 10,000 requests, 125 concurrent connections:

Reqs/sec    6937 avg  (peak: 82,646)
Latency     p50: 2.62ms  p95: 4.24ms  p99: 1.36s
HTTP 2xx    10,000 / 10,000
Throughput  2.34 MB/s

The p99 spike (1.36s) is expected — those are cache misses hitting the data source cold.

Trade-offs

  • ✅ Fast reads: L1 hit returns in microseconds
  • ✅ Scalable: Redis shares cache across instances
  • ✅ Simple API: GetOrCreateAsync handles all layers
  • ⚠️ Redis required: Adds infra dependency
  • ⚠️ Brief inconsistency: Each server has its own L1; may serve stale data until L1 expires
  • ⚠️ Cache invalidation: Not trivial — design expiry strategy upfront

References