← Back to all posts

HybridCache in .NET: Fast L1 + Shared L2 Without the Usual Caching Pain

HybridCache in .NET: Fast L1 + Shared L2 Without the Usual Caching Pain

Reading Time: 10 minutes

The Problem

Many APIs and web apps spend too much time re-fetching data that was already fetched moments ago.

Typical symptoms:

  • repeated reads for the same product, tenant config, or user profile
  • high p95/p99 latency during traffic spikes
  • cache inconsistencies between app instances
  • complex and duplicated cache code spread across services

Teams often start with IMemoryCache (fast, simple), then later add Redis (IDistributedCache) for multi-node consistency. That usually means maintaining two cache layers manually:

  • local cache rules
  • distributed cache rules
  • serialization rules
  • invalidation rules
  • concurrency controls to avoid cache stampedes

That manual approach works, but it becomes brittle quickly.

The Solution

Use HybridCache.

HybridCache gives you a unified caching API that combines:

  • L1 in-memory cache (per-process, ultra-low-latency)
  • L2 distributed cache (shared across instances, commonly Redis)

It also adds key capabilities out of the box:

  • stampede protection (single-flight for concurrent misses on the same key)
  • key and tag-based invalidation APIs
  • configurable local and distributed expirations
  • serializer integration for distributed storage

For most modern ASP.NET Core services, this is the most maintainable way to get both speed and scale without custom cache orchestration code.

Description

What L1 and L2 mean in practice

  • L1 (IMemoryCache): fastest path; data is in the current process memory
  • L2 (IDistributedCache, e.g., Redis): shared path; data survives across instances and restarts

Request flow with HybridCache:

  1. Try L1.
  2. If miss, try L2.
  3. If miss, execute factory once, then write to both caches.

That gives low latency for hot keys and consistency across horizontally scaled nodes.

Latest package versions used in this article

The examples below use the latest stable versions at the time of writing:

  • Microsoft.Extensions.Caching.Hybrid 10.5.0
  • Microsoft.Extensions.Caching.StackExchangeRedis 10.0.6
  • StackExchange.Redis 2.12.14

Install:

dotnet add package Microsoft.Extensions.Caching.Hybrid --version 10.5.0
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 10.0.6
dotnet add package StackExchange.Redis --version 2.12.14

Service registration (L1 + Redis L2)

using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Caching.StackExchangeRedis;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "coding-luggage:";
});

builder.Services
    .AddHybridCache(options =>
    {
        options.MaximumPayloadBytes = 1024 * 1024; // 1 MB default payload guard
        options.MaximumKeyLength = 1024;
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(10),
            LocalCacheExpiration = TimeSpan.FromMinutes(2)
        };
    });

This setup gives:

  • short-lived local copies for very fast repeat reads
  • longer distributed retention in Redis
  • one consistent API for reads/writes/invalidation

Reading with stampede protection

GetOrCreateAsync prevents a thundering herd for a single key. If 200 requests miss the same key at once, one request runs the factory and others await that result.

using Microsoft.Extensions.Caching.Hybrid;

public sealed class ProductReadService(
    HybridCache cache,
    IProductRepository repository)
{
    public async Task<ProductDto?> GetByIdAsync(
        Guid productId,
        CancellationToken cancellationToken = default)
    {
        var cacheKey = $"product:{productId}";
        var tags = new[] { "product", $"product:{productId}" };

        var entryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(15),
            LocalCacheExpiration = TimeSpan.FromMinutes(3)
        };

        return await cache.GetOrCreateAsync(
            cacheKey,
            async token => await repository.GetByIdAsync(productId, token),
            entryOptions,
            tags,
            cancellationToken: cancellationToken);
    }
}

Tag-based invalidation across L1 and L2

When data changes, remove by key or by tag.

public sealed class ProductWriteService(
    HybridCache cache,
    IProductRepository repository)
{
    public async Task UpdateAsync(ProductDto input, CancellationToken cancellationToken = default)
    {
        await repository.UpdateAsync(input, cancellationToken);

        // Remove the specific key when you know it.
        await cache.RemoveAsync($"product:{input.Id}", cancellationToken);

        // Optionally invalidate groups.
        await cache.RemoveByTagAsync("product", cancellationToken);
    }
}

Important nuance: tag invalidation in HybridCache is logical invalidation. It marks tagged entries as stale for future reads in both local and distributed paths, but underlying physical values can remain until normal expiration.

Key design and operational tips

  • Keep keys stable and deterministic (entity:{id}, tenant:{tenantId}:feature-flags).
  • Never use raw unbounded user input as cache keys.
  • Use short LocalCacheExpiration and longer Expiration for good L1 hit rates without stale local data lingering too long.
  • Cache immutable DTOs where possible.
  • Add jitter to expirations in high-throughput systems to reduce synchronized expirations.
  • Track hit/miss, factory duration, and invalidation volume in telemetry.

When HybridCache is a strong fit

  • read-heavy APIs with horizontal scale
  • expensive but deterministic query results
  • feature/config/profile reads shared across app instances
  • services currently juggling IMemoryCache + IDistributedCache manually

Summary

HybridCache in .NET gives you a clean path to multi-level caching:

  • L1 memory speed for repeated local reads
  • L2 Redis consistency across instances
  • stampede protection for safer cache misses under load
  • tag and key invalidation APIs for practical cache coherence

If you are building on .NET 9/10 and need high-throughput reads without custom caching plumbing, HybridCache is one of the highest-leverage platform features to adopt early.

References