diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs
index a75546b6793f..38a38069b32c 100644
--- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs
+++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics;
+using System.Linq;
 using Microsoft.Extensions.Caching.Memory;
 
 namespace Microsoft.AspNetCore.OutputCaching.Memory;
@@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory;
 internal sealed class MemoryOutputCacheStore : IOutputCacheStore
 {
     private readonly MemoryCache _cache;
-    private readonly Dictionary<string, HashSet<string>> _taggedEntries = new();
+    private readonly Dictionary<string, HashSet<TaggedEntry>> _taggedEntries = [];
     private readonly object _tagsLock = new();
 
     internal MemoryOutputCacheStore(MemoryCache cache)
@@ -20,7 +21,7 @@ internal MemoryOutputCacheStore(MemoryCache cache)
     }
 
     // For testing
-    internal Dictionary<string, HashSet<string>> TaggedEntries => _taggedEntries;
+    internal Dictionary<string, HashSet<string>> TaggedEntries => _taggedEntries.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(t => t.Key).ToHashSet());
 
     public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken)
     {
@@ -30,7 +31,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken
         {
             if (_taggedEntries.TryGetValue(tag, out var keys))
             {
-                if (keys != null && keys.Count > 0)
+                if (keys is { Count: > 0 })
                 {
                     // If MemoryCache changed to run eviction callbacks inline in Remove, iterating over keys could throw
                     // To prevent allocating a copy of the keys we check if the eviction callback ran,
@@ -40,7 +41,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken
                     while (i > 0)
                     {
                         var oldCount = keys.Count;
-                        foreach (var key in keys)
+                        foreach (var (key, _) in keys)
                         {
                             _cache.Remove(key);
                             i--;
@@ -74,6 +75,8 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val
         ArgumentNullException.ThrowIfNull(key);
         ArgumentNullException.ThrowIfNull(value);
 
+        var entryId = Guid.NewGuid();
+
         if (tags != null)
         {
             // Lock with SetEntry() to prevent EvictByTagAsync() from trying to remove a tag whose entry hasn't been added yet.
@@ -90,27 +93,27 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val
 
                     if (!_taggedEntries.TryGetValue(tag, out var keys))
                     {
-                        keys = new HashSet<string>();
+                        keys = new HashSet<TaggedEntry>();
                         _taggedEntries[tag] = keys;
                     }
 
                     Debug.Assert(keys != null);
 
-                    keys.Add(key);
+                    keys.Add(new TaggedEntry(key, entryId));
                 }
 
-                SetEntry(key, value, tags, validFor);
+                SetEntry(key, value, tags, validFor, entryId);
             }
         }
         else
         {
-            SetEntry(key, value, tags, validFor);
+            SetEntry(key, value, tags, validFor, entryId);
         }
 
         return ValueTask.CompletedTask;
     }
 
-    void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor)
+    private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor, Guid entryId)
     {
         Debug.Assert(key != null);
 
@@ -120,22 +123,25 @@ void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor)
             Size = value.Length
         };
 
-        if (tags != null && tags.Length > 0)
+        if (tags is { Length: > 0 })
         {
             // Remove cache keys from tag lists when the entry is evicted
-            options.RegisterPostEvictionCallback(RemoveFromTags, tags);
+            options.RegisterPostEvictionCallback(RemoveFromTags, (tags, entryId));
         }
 
         _cache.Set(key, value, options);
     }
 
-    void RemoveFromTags(object key, object? value, EvictionReason reason, object? state)
+    private void RemoveFromTags(object key, object? value, EvictionReason reason, object? state)
     {
-        var tags = state as string[];
+        Debug.Assert(state != null);
+
+        var (tags, entryId) = ((string[] Tags, Guid EntryId))state;
 
         Debug.Assert(tags != null);
         Debug.Assert(tags.Length > 0);
         Debug.Assert(key is string);
+        Debug.Assert(entryId != Guid.Empty);
 
         lock (_tagsLock)
         {
@@ -143,7 +149,7 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st
             {
                 if (_taggedEntries.TryGetValue(tag, out var tagged))
                 {
-                    tagged.Remove((string)key);
+                    tagged.Remove(new TaggedEntry((string)key, entryId));
 
                     // Remove the collection if there is no more keys in it
                     if (tagged.Count == 0)
@@ -154,4 +160,6 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st
             }
         }
     }
+
+    private record TaggedEntry(string Key, Guid EntryId);
 }
diff --git a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs
index e8c809911add..c1ad1d708f4b 100644
--- a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs
+++ b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs
@@ -197,6 +197,43 @@ public async Task ExpiredEntries_AreRemovedFromTags()
         Assert.Single(tag2s);
     }
 
+    [Fact]
+    public async Task ReplacedEntries_AreNotRemovedFromTags()
+    {
+        var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow };
+        var cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1000, Clock = testClock, ExpirationScanFrequency = TimeSpan.FromMilliseconds(1) });
+        var store = new MemoryOutputCacheStore(cache);
+        var value = "abc"u8.ToArray();
+
+        await store.SetAsync("a", value, new[] { "tag1", "tag2" }, TimeSpan.FromMilliseconds(5), default);
+        await store.SetAsync("a", value, new[] { "tag1" }, TimeSpan.FromMilliseconds(20), default);
+
+        testClock.Advance(TimeSpan.FromMilliseconds(10));
+
+        // Trigger background expiration by accessing the cache.
+        _ = cache.Get("a");
+
+        var resulta = await store.GetAsync("a", default);
+
+        Assert.NotNull(resulta);
+
+        HashSet<string> tag1s, tag2s;
+
+        // Wait for the tag2 HashSet to be removed by the background expiration thread.
+
+        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+        while (store.TaggedEntries.TryGetValue("tag2", out tag2s) && !cts.IsCancellationRequested)
+        {
+            await Task.Yield();
+        }
+
+        store.TaggedEntries.TryGetValue("tag1", out tag1s);
+
+        Assert.Null(tag2s);
+        Assert.Single(tag1s);
+    }
+
     [Theory]
     [InlineData(null)]
     public async Task Store_Throws_OnInvalidTag(string tag)