When caching objects in .NET using Microsoft.Extensions.Caching.Memory.MemoryCache, memory management is a common concern. A frequent question developers ask is: Once an item’s expiration policy is satisfied, is it immediately eligible for Garbage Collection (GC)?

The short answer is: No, not immediately. An expired item does not automatically become available for Garbage Collection until the cache formally evicts it and releases its internal reference to the object.

How MemoryCache Eviction Actually Works

In .NET, MemoryCache does not attach an isolated background timer to every cached entry. If it did, having thousands or millions of cached items would lead to catastrophic timer thread overhead. Instead, MemoryCache removes expired items using two main mechanisms:

  • Lazy (On-Demand) Eviction: When your application calls an accessor method like Get or TryGetValue for an item whose expiration timestamp has passed, the cache checks the validity, detects that the entry is expired, removes the reference, and returns null or false.
  • Periodic Background Scans: MemoryCache maintains an internal background timer to clean up expired entries that are never queried again. By default, this scan occurs roughly once every minute.

When Is Expiration Checked If the Key Is Never Read?

If you insert an item that expires in 5 minutes and you don't read that key again for 4 hours, it will eventually be evicted, thanks to the cache's background scan mechanism configured by ExpirationScanFrequency.

By default, MemoryCacheOptions.ExpirationScanFrequency is set to 1 minute. When that timer fires, MemoryCache traverses a segment of its entries, evicts expired ones, and triggers any registered post-eviction callbacks (RegisterPostEvictionCallback). Once evicted, the cache drops its internal reference, making the object eligible for garbage collection on subsequent GC cycles.

Proving It: Testing GC and Weak References

You can verify this behavior using a WeakReference. If an object is evicted and no other roots hold a reference to it, GC.Collect() will reclaim it.

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Caching.Memory;

var cacheOptions = new MemoryCacheOptions
{
    // Configure how often the background cleaner scans for expired items
    ExpirationScanFrequency = TimeSpan.FromSeconds(2)
};

var cache = new MemoryCache(cacheOptions);
WeakReference weakRef = AddItem(cache);

Console.WriteLine("Waiting for expiration and background cleanup...");
// Wait long enough for both item expiration (2s) and cache scan (2s)
Thread.Sleep(5000);

// Force garbage collection
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

if (weakRef.IsAlive)
{
    Console.WriteLine("Object is still in memory (still referenced by cache).");
}
else
{
    Console.WriteLine("Object was successfully garbage-collected!");
}

[MethodImpl(MethodImplOptions.NoInlining)]
static WeakReference AddItem(IMemoryCache cache)
{
    var heavyPayload = new byte[1024]; // Sample payload
    var reference = new WeakReference(heavyPayload);

    cache.Set("sampleKey", heavyPayload, new MemoryCacheEntryOptions()
        .SetAbsoluteExpiration(TimeSpan.FromSeconds(2))
        .RegisterPostEvictionCallback((key, value, reason, state) =>
        {
            Console.WriteLine($"Evicted '{key}' due to: {reason}");
        }));

    return reference;
}

Key Takeaways and Best Practices

  • Expiration != Collection: Setting an expiration date does not automatically notify the GC. The entry must first be evicted so that the cache reference is released.
  • Tune ExpirationScanFrequency: If your application caches large volumes of temporary, heavy objects and you cannot wait for periodic cleanup, you can lower MemoryCacheOptions.ExpirationScanFrequency. Be mindful that scanning too frequently increases CPU overhead.
  • Set a Size Limit: Relying solely on time-based expiration can risk an OutOfMemoryException under high load. Always configure SizeLimit and assign a Size to cache entries when dealing with memory-sensitive workloads.