Patch release: full-codebase performance pass with zero public API changes, plus three bug fixes found during the audit. Compatible drop-in upgrade from v13.0.0.
⚡ Performance
Connection pool (hot path of every operation)
- LeastLoaded no longer re-evaluates
TotalOutstanding()per comparison — each call allocated aServerCounterssnapshot in SE.Redis, up to ~9 per single Redis operation with the default pool size. Now at most one call per pooled connection. - Debug log arguments are no longer evaluated when logging is disabled (one more
GetCounters()saved per operation). - RoundRobin now uses a lock-free
Interlockedcounter: previously it used a cryptographic RNG on net6+ and allocatednew Random()per call on netstandard2.1 — and wasn't actually round-robin. - Removed a static lock held during blocking connection creation that serialized pool startup process-wide with multiple named clients.
Core
Db0..Db16/GetDefaultDatabase()no longer allocate a newRedisDatabaseon every access — instances are cached per database number.HashGetAsync(hashKey, string[])issues a single HMGET instead of N parallel HGET round-trips.UpdateExpiryAsyncdrops the redundantEXISTSpre-check beforeEXPIRE— half the round-trips, N-fold forUpdateExpiryAllAsync.AddAllAsyncbuilds its payload in one pre-sized pass instead of a LINQ pipeline with growth buffers.SetPopAsync(count),SortedSetRangeByScoreAsync,HashKeysAsync,SearchKeysAsyncnow materialize results eagerly — previously a lazySelectre-deserialized (or re-allocated) everything on each enumeration.- Tag writes serialize the key once instead of once per tag.
- Pub/Sub message handling drops ~half its per-message allocations (
ContinueWithcontinuation removed).
Serializers
- Newtonsoft and ServiceStack no longer materialize every payload as an intermediate UTF-16 string (~2× the bytes, LOH for large payloads); Newtonsoft also caches its
JsonSerializerand pools char buffers. - MsgPack and Protobuf drop their wrapping
MemoryStreams. - System.Text.Json, MemoryPack, Utf8Json were already optimal.
Compressors
- Snappier:
Decompressno longer duplicates the output array (the copy was 100% redundant);Compressrents its transient worst-case buffer fromArrayPool. - Brotli: one-shot
BrotliEncoder.TryCompressreplaces the stream pipeline. - ZstdSharp: compression contexts are now reused per thread instead of being created per call — the main zstd anti-pattern — plus exact-size decompression.
- GZip: pre-sized buffers.
ASP.NET Core
IDistributedCacheadapter: cached field arrays (no per-Get allocation) and batched HSET+EXPIRE in a single network write onSetAsync.
🐛 Bug fixes
HashGetAllAsyncAtOneTimeAsyncwith KeyPrefix: the per-call Lua script embedded the hash key unprefixed while wrongly prefixing the hash fields — with a prefix configured the method read the wrong hash. The script was also a Lua injection vector and defeated the server-side script cache. Replaced by HMGET.Utf8JsonSerializer.Deserialize(null)threw instead of returningdefault, unlike every other serializer.ListGetFromRightAsynchad a duplicated null check (dead code).
Behavioral notes
HashGetAsync(string[])returns aDictionaryinstead ofConcurrentDictionary(contract isIDictionary<string, T?>).- RoundRobin selection is now sequential instead of random.
- Pub/Sub handler errors log the original exception instead of the wrapping
AggregateException.
Full changelog: v13.0.0...v13.0.1