.NET 11 MemoryCache metrics remove the need to wrap every cache call just to count hits and misses. Enable statistics, subscribe to the built-in meter, and your existing IMemoryCache can expose requests, evictions, entry count, and estimated size through OpenTelemetry.

The important production detail is that the metrics are opt-in. You must enable TrackStatistics, register the exact meter name, and treat size as an application-defined unit. This guide shows the complete ASP.NET Core setup and the operational limits that keep the resulting dashboard honest.

Version note: .NET 11 is still in preview at the time of writing. The runtime source pinned in the references emits dotnet.cache.request.result, while the current Preview 7 “What’s new” page lists dotnet.cache.request.type. Pin the SDK and runtime build you deploy, then inspect the emitted tags before wiring dashboards or alerts because preview telemetry contracts can still change.

Why .NET 11 MemoryCache metrics matter

A cache can look healthy while quietly wasting memory or sending most requests back to the origin. Before .NET 11, teams often used GetCurrentStatistics(), a decorator, or application-specific counters. Those approaches can work, but they create code that must be kept consistent with the cache and with every access path.

.NET 11 moves the common signals into MemoryCache itself. When statistics are enabled, the cache publishes observable instruments from the Microsoft.Extensions.Caching.Memory.MemoryCache meter. OpenTelemetry can collect them without changing the code that calls Get, Set, or TryGetValue.

This is observability, not a cache policy. The new instruments tell you what happened; they do not select expiration times, prevent cache stampedes, or enforce a memory budget. For those concerns, start with expiration and invalidation safety and the reusable cache service with stampede protection.

Enable metrics in ASP.NET Core

The smallest useful setup has two independent switches: cache statistics and meter collection. If either is missing, the dashboard stays empty.

using OpenTelemetry.Metrics;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMemoryCache(options =>
{
    options.Name = "Products";
    options.TrackStatistics = true;
    options.SizeLimit = 100_000;
});

builder.Services
    .AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
        metrics
            .AddMeter("Microsoft.Extensions.Caching.Memory.MemoryCache")
            .AddAspNetCoreInstrumentation()
            .AddPrometheusExporter();
    });

var app = builder.Build();

app.MapPrometheusScrapingEndpoint();
app.Run();

The exporter and instrumentation extension methods require the corresponding OpenTelemetry packages. Keep package versions aligned with the OpenTelemetry dependency set used by your application instead of copying a version number from an article.

TrackStatistics defaults to false. Turning it on makes MemoryCache retain the counters used by both GetCurrentStatistics() and the observable instruments. Give the cache a stable Name; it becomes the dotnet.cache.name tag and is the dimension you use to separate cache instances.

Use a real size contract

SizeLimit is not bytes unless your application defines it as bytes. It is a unit budget. Every entry added to a size-limited cache must specify its size using the same unit; otherwise the cache cannot make a meaningful admission or compaction decision.

public sealed class ProductSnapshotCache(IMemoryCache cache)
{
    public async Task<ProductSnapshot> GetAsync(
        int productId,
        CancellationToken cancellationToken)
    {
        string key = $"product:{productId}";

        return await cache.GetOrCreateAsync(key, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
            entry.SlidingExpiration = TimeSpan.FromMinutes(2);
            entry.Size = 1;

            return await LoadProductAsync(productId, cancellationToken);
        }) ?? throw new InvalidOperationException("Product was not loaded.");
    }

    private static Task<ProductSnapshot> LoadProductAsync(
        int productId,
        CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

Here the budget is entry count, so every product snapshot has size 1. A payload-oriented cache could use approximate bytes instead, but it must do so consistently.

Understand the instruments and tags

InstrumentTypeMeaning
dotnet.cache.requestsObservable counterCumulative lookups, split into hit and miss measurements
dotnet.cache.evictionsObservable counterCumulative entries evicted by the cache
dotnet.cache.entriesObservable up/down counterCurrent number of entries
dotnet.cache.estimated_sizeObservable gaugeCurrent sum of application-defined entry sizes

Every measurement includes dotnet.cache.name. Request measurements also include dotnet.cache.request.result with either hit or miss. Keep those dimensions bounded. Cache names should describe long-lived cache roles, not tenants, users, request IDs, or keys.

The estimated-size instrument is emitted only when the cache tracks size. In the standard implementation, that means a SizeLimit is configured. If you do not have a consistent size model, use entry count and eviction behavior instead of pretending the gauge represents memory consumption.

Export through OpenTelemetry

The meter produces signals inside the process. OpenTelemetry decides which instruments to collect, how often to observe them, and where to export them. Prometheus is convenient for a local scrape endpoint; OTLP is usually the better fit when an organization already sends telemetry through a collector.

builder.Services
    .AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
        metrics
            .AddMeter("Microsoft.Extensions.Caching.Memory.MemoryCache")
            .AddOtlpExporter();
    });

Do not assume the backend displays the original dotted instrument names. Exporters commonly normalize names, and some backends append type-specific suffixes. Verify the exported series in your collector or metrics explorer before committing dashboards and alerts.

Verify the signal before production

Exercise a deterministic key before trusting the dashboard. Request the uncached key once, request the same key again, and then scrape the metrics endpoint or inspect the collector. The first call should advance the miss measurement; the second should advance the hit measurement for the same dotnet.cache.name. Remove a test entry or force a bounded eviction separately when you need to verify the eviction series.

curl --fail http://localhost:5000/products/42
curl --fail http://localhost:5000/products/42
curl --fail http://localhost:5000/metrics

The sample endpoint names are placeholders for your application. The observable evidence is a hit/miss change after known requests, not merely the presence of a meter registration in startup code.

Multiple cache instances

The default ASP.NET Core registration resolves MemoryCache through dependency injection. When OpenTelemetry supplies an IMeterFactory, the cache creates its meter through that factory; otherwise it falls back to a shared process-wide meter. Either way, dotnet.cache.name is the stable selector for an instance.

If you construct MemoryCache manually, prefer the constructor that accepts IMeterFactory when you want the instance to participate in the application’s managed metrics pipeline. Also dispose manually created caches when their owning component ends.

Build useful dashboards and alerts

Raw cumulative totals are rarely actionable. Derive rates and ratios over a window that matches the traffic pattern:

  • Hit ratio: hit request rate divided by the sum of hit and miss request rates.
  • Miss rate: misses per second, correlated with origin latency and error rate.
  • Eviction rate: evictions per second, compared with entries and estimated size.
  • Occupancy: estimated size divided by the configured size limit when both use the same unit.

A low hit ratio is not automatically a defect. One-time keys, invalidation-heavy data, cold deployments, and deliberately short lifetimes all reduce it. Alert on a sustained change from a known baseline and pair it with a user-visible consequence such as increased database duration or downstream request volume.

Evictions need the same context. A steady eviction rate near a stable occupancy can indicate a healthy bounded cache. A sharp rise in evictions, misses, and origin latency together is a stronger sign of churn or an undersized budget.

Avoid misleading cache telemetry

Metrics do not prevent stampedes

Several concurrent misses for one hot key can trigger duplicate origin work. The counters reveal the misses, but they do not serialize the factory. Apply request coalescing or another stampede-control strategy separately.

Remember the process-local boundary

IMemoryCache is local to one application process. In a scaled deployment, each replica has its own warm-up pattern, entries, and evictions. Aggregate for service health, but retain an instance dimension when diagnosing one unhealthy replica. Do not read the aggregate as if every node shares one cache.

Handle restarts and counter resets

Request and eviction instruments are cumulative. They restart with the process. Use backend rate functions that tolerate resets, and avoid comparing raw totals across deployments.

Measure the cost in your workload

Statistics add bookkeeping to cache operations. The built-in implementation is designed for this scenario, but a very hot cache should still be measured under representative concurrency. Enable the telemetry because it answers an operational question, not merely because it exists.

Production checklist

  • Enable TrackStatistics explicitly.
  • Register the exact Microsoft.Extensions.Caching.Memory.MemoryCache meter.
  • Assign a stable, low-cardinality cache name.
  • Define one consistent unit before enabling SizeLimit.
  • Confirm the exporter’s final instrument and tag names.
  • Use rates and ratios rather than alerting on cumulative totals.
  • Correlate cache signals with origin latency, errors, and deployment events.
  • Load-test the effect of statistics for a high-throughput cache.

The useful change in .NET 11 is not merely four new graphs. It is a standard observation point inside the cache implementation. With stable naming and a defined size contract, the same signals can support capacity tuning, regression detection, and incident diagnosis without coupling application code to a custom metrics wrapper.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
100% Free SEO Tools - Tool Kits PRO