HybridCache gives ASP.NET Core applications one asynchronous API for fast in-process caching and optional shared distributed caching. It removes much of the repeated cache-aside code previously needed with IMemoryCache and IDistributedCache, adds per-process cache-stampede protection, and can use a configured distributed-cache provider such as Redis as a second-level cache. HybridCache arrived with .NET 9 and became generally available on March 12, 2025 through the Microsoft.Extensions.Caching.Hybrid package.

Key takeaways

  • HybridCache checks a local in-memory cache first, then an optional distributed cache, and finally runs application code to retrieve missing data.
  • GetOrCreateAsync replaces the repetitive get, deserialize, load, serialize, and set sequence common with IDistributedCache.
  • For a given key and one HybridCache instance, concurrent cache misses share one factory operation instead of all querying the database.
  • Redis is optional: without an IDistributedCache registration, HybridCache still provides local caching and stampede protection.
  • Local caches belong to individual application processes, so a multi-server application still needs careful expiration and invalidation design.

What did ASP.NET Core 9 add with HybridCache?

Before HybridCache, ASP.NET Core developers usually chose between two useful but incomplete abstractions. IMemoryCache keeps objects in the memory of one application process, which makes reads fast but means each server has a separate cache. IDistributedCache supports a shared external store, but applications commonly had to manage byte arrays, serialization, cache misses, and writes themselves.

HybridCache is a higher-level abstraction that combines those approaches. Microsoft described the .NET 9 design as a limited in-process L1 cache backed by a separate out-of-process L2 cache, with configurable serialization and protection against duplicate work when many requests miss the same entry at once. The API was designed to cover common distributed-cache scenarios while offering a simpler cache-aside workflow. Microsoft’s Build 2024 announcement introduced that architecture.

The most visible API change is that an application supplies the work required to load a value. It does not first call GetAsync, inspect a nullable byte array, deserialize it, and later call SetAsync. Instead, it calls GetOrCreateAsync with a cache key and a factory function. HybridCache decides whether the factory must run.

ApproachWhere data livesWhat application code commonly manages
IMemoryCacheOne application processCache lookup, expiration, and behavior after a restart or scale-out event
IDistributedCacheAn external cache such as RedisLookup, serialization, deserialization, writes, and concurrent misses
HybridCacheLocal memory plus an optional distributed cacheKeys, expiration choices, and the factory that retrieves authoritative data

This does not mean caching becomes automatic or correct by default. The application still owns the cache key scheme, data lifetime, source-of-truth updates, and authorization boundaries. HybridCache reduces plumbing; it does not decide which data is safe to cache.

How does HybridCache find a cached value?

Multiple requests sharing one cache refresh operation.

Stampede protection coordinates concurrent requests for the same missing value.

HybridCache uses a layered lookup. Its primary cache is MemoryCache by default. If the value is absent there and an out-of-process IDistributedCache implementation is registered, HybridCache checks that secondary store. Only after both lookups miss does it run the supplied factory, then store the resulting value in the applicable cache layers. Microsoft’s HybridCache documentation describes this read path and configuration model.

What are L1 and L2 caches?

L1 is the local memory cache in the current web-server process. It avoids network traffic and serialization when that process has the requested value. L2 is an optional distributed cache, commonly Redis, that can be shared by several application instances.

Consider three API instances behind a load balancer. Each instance has its own L1 cache. A Redis L2 cache lets an instance that has just started retrieve a previously cached value without querying the database, after which it can populate its own local L1 cache. The local cache is still not shared: it is lost if that specific process restarts.

What is cache-stampede protection?

A cache stampede happens when many requests miss or expire the same key at nearly the same time. Without coordination, every request can issue the same expensive database query or HTTP call. HybridCache coordinates callers using the same HybridCache instance and the same key: one caller runs the factory, while the others wait for that result.

The scope matters. Microsoft documents that this coordination does not extend across separate HybridCache instances, even if they point to the same distributed cache. In a multi-instance deployment, it is still possible for more than one server to refresh a cold key concurrently. That limitation is important for expensive global operations or strict rate limits. The official documentation also notes that the factory receives a cancellation token representing the combined interest of the callers waiting on that shared operation.

How can a beginner add HybridCache and Redis?

Install the HybridCache package first. If the application will use Redis as L2, install the StackExchange Redis cache provider as well. The Redis connection string should be stored in configuration or a secret store, not hard-coded into source code.

C#
dotnet add package Microsoft.Extensions.Caching.Hybrid
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Next, register Redis as the IDistributedCache provider and register HybridCache. The following configuration gives distributed entries a 10-minute lifetime and gives entries in the local cache a shorter one-minute lifetime. Shorter local expiration can reduce the time one server continues serving an older local copy after data changes.

C#
using Microsoft.Extensions.Caching.Hybrid;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration
        .GetConnectionString("RedisConnectionString");
});

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

var app = builder.Build();
app.Run();

The defaults shown above match documented guardrails: the default maximum payload is 1 MB and the default maximum key length is 1,024 characters. When a value exceeds the payload limit, HybridCache logs the condition and does not store that value. These limits help prevent unexpectedly large entries or keys from consuming cache capacity. Microsoft documents both options and their default values.

For a single-server application or an early prototype, remove the Redis registration and keep AddHybridCache. The application will use only in-process caching while retaining the same GetOrCreateAsync calling pattern. Independent tutorials from Trailhead Technology Partners and Code Maze likewise show registration followed by a single cache access API.

How does a real product lookup use GetOrCreateAsync?

The following service caches a product DTO loaded through Entity Framework Core. The key contains a stable internal identifier and a clear prefix. The factory runs only when neither cache level contains a valid entry, so the database query is not part of a normal cache hit.

C#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;

public sealed record ProductDto(int Id, string Name, decimal Price);

public sealed class ProductService(
    AppDbContext db,
    HybridCache cache)
{
    public async Task<ProductDto> GetAsync(
        int id,
        CancellationToken cancellationToken = default)
    {
        var options = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(10),
            LocalCacheExpiration = TimeSpan.FromMinutes(1)
        };

        return await cache.GetOrCreateAsync(
            $"product:{id}",
            async ct =>
            {
                var product = await db.Products
                    .AsNoTracking()
                    .SingleAsync(p => p.Id == id, ct);

                return new ProductDto(product.Id, product.Name, product.Price);
            },
            options: options,
            tags: ["products", $"product:{id}"],
            cancellationToken: cancellationToken);
    }

    public async Task InvalidateAsync(
        int id,
        CancellationToken cancellationToken = default)
    {
        await cache.RemoveAsync($"product:{id}", cancellationToken);
    }
}

This is cache-aside behavior expressed in one operation. On a hit, HybridCache returns the cached DTO. On a miss, it calls the lambda, writes the result to the local cache and, when Redis is configured, to the distributed cache.

Keys must uniquely identify the data and must not use arbitrary raw user input. For example, a key such as product:42 is clear, while ambiguous concatenation can create collisions. Microsoft specifically recommends delimiters and trusted identifiers, and warns that raw external input in keys can create authorization and cache-flooding risks. See the cache-key guidance.

How should an application invalidate HybridCache entries?

Three application servers with local caches connected to a shared cache.

Each server has its own local cache while a distributed layer can be shared.

Expiration is necessary but is not enough when a user immediately changes product data. After saving an update to the authoritative database, remove the exact key with RemoveAsync. HybridCache removes a key from the current server’s primary cache and from the secondary cache when one is configured.

Tags offer a grouping mechanism. In the example, every product is tagged with products and its own product tag. An administrator could invalidate a group after a catalog-wide change:

C#
await cache.RemoveByTagAsync("products", cancellationToken);

Tag invalidation has a subtle but important meaning: it is logical invalidation, not an immediate physical deletion of every cached value. HybridCache records that entries created before the tag invalidation must be ignored. Those older payloads remain in memory or Redis until their normal expiration. The wildcard tag * invalidates all HybridCache data, so it should be used cautiously. Microsoft’s tag-invalidation documentation explains this behavior.

There is also a scale-out limitation: invalidating an entry affects the current server and secondary cache, but other servers’ local L1 entries are not immediately removed by the default model. Use modest local lifetimes when freshness matters, and do not promise instant cross-node consistency unless the application architecture explicitly provides it.

What trade-offs should beginners understand?

HybridCache is asynchronous. Its central API is GetOrCreateAsync, which is appropriate because a secondary cache or a data source can involve network I/O. Existing synchronous code may need an asynchronous refactor rather than a mechanical one-line replacement.

Serialization still matters for L2. HybridCache handles string and byte[] internally and uses System.Text.Json for other types by default. Developers can configure type-specific serializers or serializer factories, including protobuf-based choices. DTOs kept in a distributed cache should be deliberately versioned and should not expose data that a different caller is not authorized to receive.

Object reuse is opt-in for a reason. Traditional distributed-cache reads generally deserialize a new object per caller, which avoids callers accidentally modifying the same instance. HybridCache preserves that behavior by default. Microsoft allows reuse optimization for types that are immutable and marked appropriately, but mutable cached objects should not be shared. The HybridCache guidance recommends sealing and marking safely reusable immutable types.

Cache only data that can be safely stale. Good beginner candidates include public product details, lookup lists, configuration read frequently by a service, and costly read-only API responses. Avoid caching per-user or security-sensitive responses under keys that fail to include the relevant identity and authorization context. The database or another authoritative service remains the source of truth.

How should a team get started with HybridCache?

  1. Identify one read-heavy endpoint whose underlying query is measurable and whose data tolerates a defined period of staleness.
  2. Create a clear, versionable cache key format using trusted identifiers, such as product:42 or catalog:v1:featured.
  3. Register AddHybridCache and begin with an explicit short expiration rather than caching indefinitely.
  4. Wrap the existing read in GetOrCreateAsync, passing the request cancellation token.
  5. Add exact-key removal after updates, then introduce tags only where group invalidation is genuinely useful.
  6. For a scaled-out service, add Redis as L2 and test cold starts, expiration, update races, and Redis outages before relying on the cache for a critical path.

HybridCache is most valuable when it replaces repeated cache-aside boilerplate without hiding the architectural decisions that still matter. Start with one bounded use case, observe hit rates and database load, and expand only after the application has a clear freshness and invalidation policy.

Frequently asked questions

Is HybridCache part of ASP.NET Core 9?

HybridCache was introduced with .NET 9 as the Microsoft.Extensions.Caching.Hybrid library. Microsoft announced that the package became generally available on March 12, 2025.

Does HybridCache require Redis?

No. Without a registered IDistributedCache provider, HybridCache uses in-process memory and still provides stampede protection. Redis or another out-of-process provider is needed when several application instances should share an L2 cache.

Does HybridCache prevent every cache stampede in a multi-server app?

No. It ensures one factory execution per key among concurrent callers using the same HybridCache instance. That coordination does not extend across separate application processes, even when they use the same distributed cache.

What is the difference between RemoveAsync and RemoveByTagAsync?

RemoveAsync removes a specific key. RemoveByTagAsync logically invalidates all entries associated with a tag, so older entries are ignored on later reads even though their stored payloads remain until expiration.

Can HybridCache cache any object?

It can cache strings and byte arrays directly and uses System.Text.Json by default for other types when serialization is required for L2. Applications can configure other serializers, but cached types should be safe to serialize, appropriately scoped, and treated as potentially stale.

Sources