NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Go modules
Last release today
28 Sep 2026
Ships on a steady schedule
a new release about every 9 days
Some releases are documented
notes for 17 of 43 stable releases
5 versions withdrawn
withdrawn after publishing
4 years old
504 releases · first in 2023
One column per quarter.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Security policy : send vulnerability reports to the Redis VDP ( #3949 ) by @ndyakov
This is a beta release. You can upgrade without changes to your code. The release adds three primary features:
The release also contains many stability fixes and robustness fixes.
⚠️ This release changes one default behavior. Each Client (this includes failover clients and cluster node clients) now creates a small, dedicated pipeline connection pool (#4002). Pipeline, TxPipeline, and autopipeline operations use this pool. These operations do not compete with regular commands for main-pool connections. The pool supplies burst capacity only:
Set PipelinePoolSize: -1 to get the previous single-pool behavior.
Set AutoPipelineOptions.FullDuplex to enable the full-duplex mode of the automatic pipeliner. The default mode sends one batch for each round trip. The full-duplex mode is different: the engine holds one pipeline-pool connection, and a writer goroutine and a reader goroutine move the ordered command stream in the two directions at the same time. On a link with high latency, each command completes in approximately one round-trip time (RTT) on a single connection. A test on a 50 ms WAN profile measured ~389k operations/s at 52 ms p50. The half-duplex ordered path measured ~207k operations/s at 116 ms.
The mode operates on the two faces of a standalone Client: AutoPipeline() and AsyncAutoPipeline(). The mode also operates natively on a ClusterClient if the routing goes to masters only. A child engine for each master sends each command to the node that owns the command's slot. The engine obeys MOVED and ASK redirects and retryable replies (LOADING, READONLY, ...) through the usual cluster redirect procedure. If replica routing is set (ReadOnly, RouteByLatency, or RouteRandomly), the autopipeliner uses the half-duplex shard flushers, which obey the shard picker. Config().FullDuplex reports the mode that is in effect.
These options tune the mode:
FullDuplexWindow — the maximum number of commands in flight (backpressure).FullDuplexIdleTimeout and FullDuplexMaxHold — control when the engine returns the held connection to the pool. The pool hooks then do the re-authentication and the maintenance-notification handoffs.FullDuplexFastSubmit — an optional fast submit path for many producers on low-RTT links.The full-duplex path supports blocking commands, Options.Limiter (one admission for each written batch), per-command hooks, OTel metrics, retry budgets (the client never sends a NoRetry command again after the command is on the wire), and seamless maintenance handoffs. The stream model causes these limits:
ProcessHook that returns without a call to next does not cancel the command on the full-duplex path. The command is already in the queue of the held connection, and the client sends it. Run policy hooks and kill-switch hooks on a plain client or on the half-duplex autopipeliner.HIMPORT commands. A diverted command has no order relation to the stream. On the async face, wait for the result of a diverted command before you submit a command that depends on it.rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
AutoPipelineOptions: &redis.AutoPipelineOptions{
FullDuplex: true, // stream commands on one held connection, ~1 RTT each
},
})
defer rdb.Close()
// Deferred face: calls return immediately; result accessors block until executed.
ap, err := rdb.AsyncAutoPipeline()
if err != nil {
panic(err)
}
cmds := make([]*redis.StatusCmd, 0, 1000)
for i := 0; i < 1000; i++ {
cmds = append(cmds, ap.Set(ctx, fmt.Sprintf("key:%d", i), i, 0))
}
for _, cmd := range cmds {
if err := cmd.Err(); err != nil {
// handle error
}
}
// Or the blocking face — a drop-in Cmdable where each call blocks like a
// plain client while concurrent callers share the full-duplex pipe:
// ap, err := rdb.AutoPipeline()
// val, err := ap.Get(ctx, "key").Result()The example/autopipeline directory contains a runnable tour of all the autopipeliner faces.
Two related defaults changed. AutoPipelineOptions.MaxBatchBytes now has a default of 128 KiB (before, the size had no limit). This default prevents a write/reply deadlock. It is not a throughput control. The buffers of the pipeline pool also have a default of 128 KiB.
Experimental: the auto-pipelining APIs can change in a minor release.
The release adds two functions to the experimental shared-tracking client-side cache. The functions have no effect unless CSC is enabled (#3989) by @ndyakov:
Options.ClientSideCacheRefreshOnInvalidate): the client reads recently-read keys again in the background immediately after their invalidation push arrives. The next reader does not pay the cache miss. The client collects invalidated hot keys in a short window and reads them again on a pipelined connection. ClientSideCacheRefreshRecencyWindow sets which entries count as recently read. ClientSideCacheInvalidationBatchWindow collects invalidation-driven cache deletes into background batches; without it, the connection reader applies each delete inline.Options.ClientSideCacheCoalesceMisses): the client pipelines concurrent cache-miss reads onto one tracked connection. Each miss keeps the caller's own per-key command. The client does not rewrite the commands to MGET, so the function is safe on a cluster. The client writes each reply to the cache with the tracking generation of the connection. The server can thus invalidate each entry.These options have the same requirement as the other CSC options: the built-in cache (ClientSideCacheConfig, or ClientSideCache set to a *LocalCache). The client ignores the options if a custom Cache implementation is set.
RouteByLatency selects the node with the strictly minimum latency. The latency measurement has noise: the value is the mean of ten pings, and the client refreshes it at most each 10 s. Thus all clients can select the same node from a set of nodes that have almost equal latency. A production system showed this problem: the GET rates across a five-replica set in one availability zone had a 590x spread.
The new ClusterOptions.RouteByLatencyTolerance widens the selection to each node with a latency in the tolerance above the fastest node. The client distributes reads across these nodes with the round-robin procedure of the ShardPicker. A node in a different availability zone stays outside a sensible tolerance, so zone locality is kept. The default is zero, which keeps the strict-minimum behavior. The option is also on FailoverOptions, where it applies to clients from NewFailoverClusterClient. The plain NewFailoverClient does not support latency routing. (#3973) by @jozenstar
The same work corrected a routing defect. The client recorded the nearest healthy node only when that node was also the fastest node overall. A node that fails fast (a refused connection fails fast, so this is frequent) thus hid each healthy node, and the client sent reads to the node that failed. The client now records the healthy minimum separately. (#3994) by @jozenstar
AutoPipelineOptions.FullDuplex, with FullDuplexWindow / FullDuplexIdleTimeout / FullDuplexMaxHold / FullDuplexFastSubmit. Available on standalone clients and cluster clients (#4002) by @ndyakovPipelinePoolSize has a default of DefaultPipelinePoolSize (10) on each client. If the pool is full, an operation immediately uses the main pool. Set -1 to disable the pool (#4002, #3959) by @ndyakovOptions.ClientSideCacheRefreshOnInvalidate (with ClientSideCacheRefreshRecencyWindow / ClientSideCacheInvalidationBatchWindow) and Options.ClientSideCacheCoalesceMisses (#3989) by @ndyakovRouteByLatencyTolerance: distributes reads across nodes that have almost equal latency, on cluster clients and on NewFailoverClusterClient (#3973) by @jozenstarAutoPipeliner.WaitClosed: blocks until the drain of the accepted commands completes, and returns the drain result. Use it in a wrapper that must not close shared pools while a flush is in progress (#3998) by @ndyakovCMSInfo.CellSize: contains the cell-size field of CMS.INFO in Redis 8.12 (#4010) by @elena-kolevska*.INFO forward compatibility: the parsers for BF.INFO / CF.INFO / CMS.INFO / TOPK.INFO / TDIGEST.INFO skip unknown fields and do not return an error. Redis 8.12 adds cell size to CMS.INFO (#4010) by @elena-kolevskaNewClient panic leak: a panic during construction (for example, a maintnotifications failure in ModeEnabled) does not leak the connection pools that already exist. A typed-nil pool cannot hide the initial panic (#4003) by @ndyakov. The same guards are applied to NewFailoverClient (#4002)Conn.Close does the socket teardown and the unsubscribe/CSC callbacks when the connection is already CLOSED. Before, init and auth failures collected open descriptors. The transport now closes exactly one time for each socket generation (fixes #3982) (#3985) by @ndyakovLogger and LogLevel. The call-site attribution is correct again (#3988) by @saddamr3eConn.onClose data race: the close hooks that init installs (onClose and onCscClose) are now atomic against a concurrent Close (#3966) by @saddamr3eFTHybridCmd reads the full RESP3 map reply and does not desynchronize the connection (#3956) by @saddamr3eCLIENT INFO forward compatibility: the parser skips unknown client-flag characters. The full reply does not fail (#3977) by @ndyakovGEOSEARCH duplicate args: the command does not send duplicate arguments (#3955) by @mehmettokgozMSetEX cluster routing: the constructor sets the first-key position. Typed calls thus go to the correct slot (#3984) by @shivamrustagiClose: a concurrent Close does not block (no re-entrant deadlock). WaitClosed supplies the drain result (#3998) by @ndyakovisHealthyConn shows only at the debug level. CSC invalidations do not fill the log (#3948) by @ndyakovPipeline/TxPipeline path (#4002) by @ndyakovScan gets zero-copy semantics, and the RESP reader does not do unnecessary data conversions (#3972) by @vlady-kotsevPing gate. This removes ~1.6 min of dial-retry waits in environments without the full stack (#4001) by @ndyakovCLIENT INFO tracking-flag assertion does not run behind the RE proxy (#3981) by @ndyakovWe thank all the contributors who worked on this release!
@elena-kolevska, @jozenstar, @mehmettokgoz, @ndyakov, @Phalanyx, @saddamr3e, @shivamrustagi, @vlady-kotsev
Full Changelog: v9.22.0...v9.23.0-beta.1
chore(release): release notes
chore(release): bump versions
chore(release): update release notes to ASD-STE100
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
This is a minor release introducing two flagship (experimental) features — client-side caching and automatic pipelining — alongside support for Redis
This is a minor release introducing two flagship (experimental) features — client-side caching and automatic pipelining — alongside support for Redis 8.10, new commands, and a large batch of stability and parser-robustness fixes. It consolidates everything shipped in 9.22.0-beta.1, so the notes below cover the full 9.21.0 → 9.22.0 upgrade.
⚠️ Two changes to be aware of when upgrading from 9.21.0:
WaitAOF return type corrected (#3888): WaitAOF now returns *IntSliceCmd, matching the two-integer reply of WAITAOF (previously *IntCmd, which failed to parse the reply at runtime). Code referencing the old return type needs a one-line update.The standalone Client gains server-assisted client-side caching built on RESP3 CLIENT TRACKING. Enable it by setting ClientSideCacheConfig in Options (or supply your own cache via ClientSideCache — e.g. to share one cache across clients). Cacheable read results are served from a local in-process cache and invalidated automatically when the server reports a change, cutting round trips for read-heavy workloads.
The invalidation architecture is selected by ClientSideCacheStrategy; the default (and currently only) strategy is CSCStrategySharedTracking: one shared cache, every pool connection runs plain CLIENT TRACKING ON, and a background drainer applies buffered invalidations — portable (no BCAST) and consistent with the other Redis client libraries. Requirements and guardrails: RESP3 (Protocol: 3), standalone client, DB 0 only; commands that would change the connection identity (SELECT, AUTH, ...) are rejected while caching is enabled, and CSC is disabled when a credentials provider is set (fixed Username/Password work and are namespaced). See the README's client-side caching section and the runnable example.
Experimental: the API may change in a minor release.
(#3941) by @ofekshenawa
AutoPipeliner is a background batcher that coalesces commands from many concurrent goroutines into Redis pipelines, multiplying throughput without any manual pipeline management. It comes in two faces, available on Client and ClusterClient (and configurable via Options.AutoPipelineOptions / UniversalOptions.AutoPipelineOptions):
AutoPipeline() — the blocking face: a drop-in Cmdable where each call blocks until executed, exactly like a plain client, while concurrent callers' commands batch together under the hood (measured locally over loopback: ~1M+ SET/sec vs ~100k unpipelined; indicative, not a guarantee). Per-goroutine command order is preserved.AsyncAutoPipeline() — the deferred face: command calls return immediately and every typed result accessor (Val/Result/Err/...) blocks until the command has executed. Submit a window of commands, then read the results, to keep pipelines deep (~2–3M SET/sec locally; indicative).AutoPipelineOptions controls batching: MaxBatchSize (soft target, default 200; the blocking face's preset uses 300), MaxBatchBytes (approximate payload cap so huge values flush as several bounded writes), MaxFlushDelay with optional AdaptiveDelay (delay scales down as the queue fills), and MaxConcurrentBatches (default 1 = a single ordered batch stream; raising it requires Unordered: true, so ordering is never lost by accident — Validate() rejects the combination otherwise). A usage tour and throughput comparison live in example/autopipeline.
Experimental: the API may change in a future release — pin your go-redis version if you adopt it.
(#3942) by @ndyakov, with help from @cxljs
This release adds support for Redis 8.10. The README's supported-versions list now includes Redis 8.10, and CI runs the full suite against the redislabs/client-libs-test:8.10.0 image by default (#3920, #3940).
Coverage for the new commands and options that ship with Redis 8.10:
HIMPORT (#3919) — bulk hash import via server-side fieldsets, exposed as HImportPrepare, HImportSet, HImportDiscard, and HImportDiscardAll. Fieldsets are session state scoped to a single physical connection, which does not mix well with connection pooling — so the client keeps a versioned fieldset registry and lazily replays the PREPARE on whichever pooled connection executes a SET that needs it, at most once per connection, with no extra round trip (the PREPARE is injected into the same write as the SET).LMOVEM / BLMOVEM (#3913) — move multiple elements between lists in one call.SUNIONCARD / SDIFFCARD (#3897) — cardinality of set union/difference without materializing the result.XREAD / XREADGROUP MAXCOUNT and MAXSIZE (#3898) — bound how much data a stream read returns.TS.READ (#3896), TS.QUERYLABELS (#3926), TS.NRANGE / TS.NREVRANGE (#3870) with multiple aggregators per key (#3937), and EXCLUDEEMPTY on TS.MRANGE / TS.MREVRANGE (#3912) — new time-series query surface.FT.ALIASLIST (#3925), COLLECT reducer for FT.AGGREGATE (#3886), RERANK on HNSW vector fields in FT.CREATE (#3927), and FT.HYBRID timeout warnings (#3911) — search coverage.Default configuration values now follow the cross-SDK configuration proposal shared by all Redis client libraries (#3918):
| Setting | Old default | New default |
|---|---|---|
ReadTimeout / WriteTimeout |
3s | 5s |
| Retry backoff (min/max) | 8ms / 512ms | 10ms / 1s |
| Cluster state reload interval | 10s | 60s |
| TCP keep-alive | 5min period | 30s idle / 5s interval / 3 probes (net.KeepAliveConfig) |
Applications that set these values explicitly are unaffected; applications relying on the old defaults inherit the new ones.
A systematic audit fixed data races across the client — hooks (AddHook, #3868), Ring.SetAddrs (#3862), cluster node slices (#3861), pub/sub reconnect (#3906), maintenance notifications (#3894, #3872), pool handoff (#3876), and redisotel (#3881) — and hardened the RESP parsers against malformed or unexpected replies: over-reads on nil replies (#3874), integer overflow when skipping map/attribute bodies (#3877), unhashable RESP3 map keys (#3873), odd-length flat replies (#3900), mismatched declared array lengths (#3907), unexpected extra reply frames (#3884), and nil elements in numeric/bool slice replies (#3922).
Receive Hang FixPeekPushNotificationName blocked until 36 bytes were buffered, so a short subscribe confirmation (channel name of six or fewer characters) on an otherwise idle connection hung PubSub.Receive forever — a regression introduced in 9.20.1 by #3842. The peek now parses whatever is already buffered and only waits for one more byte when the frame prefix is valid but incomplete. Fixes #3935.
The cluster transaction pipeline treated a MULTI...EXEC block as independently retryable commands, which could scatter a transaction across nodes or send malformed transactions on retry. Redirects (MOVED/ASK/TRYAGAIN) and aborts are now handled at the whole-transaction level, matching Redis transaction semantics: the transaction is re-routed and retried as a unit, never partially (#3909) by @cxljs.
rediscmd.AppendCmd — used by redisotel and rediscensus to render commands into span attributes — now redacts credential arguments as <redacted>: AUTH, HELLO ... AUTH, CONFIG SET of requirepass / masterauth / TLS key passphrases, ACL SETUSER password rules, and MIGRATE ... AUTH/AUTH2. The client sends HELLO ... AUTH on every handshake and AUTH on every streaming-credentials rotation through the regular hook chain, so tracing hooks previously captured credentials even when the application never issued an auth command itself (#3939) by @saddamr3e.
ClientSideCacheConfig / ClientSideCache, with the CSCStrategySharedTracking invalidation strategy (#3941) by @ofekshenawaAutoPipeline() (blocking) and AsyncAutoPipeline() (deferred results) on Client and ClusterClient, configured via AutoPipelineOptions (#3942) by @ndyakov, with help from @cxljsHIMPORT command family: HImportPrepare / HImportSet / HImportDiscard / HImportDiscardAll with lazy per-connection fieldset prepare replay (#3919) by @ndyakovLMOVEM / BLMOVEM: move multiple list elements in one call, with COUNT (up to N) or EXACTLY (all-or-nothing) semantics via LMoveMArgs (#3913) by @ofekshenawaSUnionCard / SDiffCard: cardinality of set union/difference (#3897) by @ofekshenawaXRead / XReadGroup MAXCOUNT / MAXSIZE: bound stream read responses by entry count or payload size (#3898) by @ofekshenawaTS.READ: read samples from a series starting at a given timestamp, with TSReadEarliest (-), TSReadLatest (+), and TSReadNew ($) sentinels (#3896) by @ofekshenawaTS.QUERYLABELS: query label names/values across time series (#3926) by @ndyakovTS.NRANGE / TS.NREVRANGE: range queries across multiple series (#3870) by @ofekshenawa, with multiple aggregators per key (#3937) by @ndyakovTS.MRANGE / TS.MREVRANGE EXCLUDEEMPTY: skip series with no samples in the result (#3912) by @ofekshenawaFT.ALIASLIST: list all index aliases (#3925) by @ndyakovFT.AGGREGATE COLLECT reducer: collect grouped values into an array (#3886) by @ndyakovFT.CREATE RERANK: RERANK parameter on HNSW vector field definitions (#3927) by @ofekshenawaFT.HYBRID timeout warnings: timeout warnings are now populated in hybrid search results (#3911) by @ofekshenawaFT.HYBRID KNN SHARD_K_RATIO (Redis 8.8+): per-shard K ratio for KNN clauses (#3841) by @ndyakovReceive hang: peek push-notification names without demanding 36 buffered bytes, fixing a hang on short subscribe confirmations (fixes #3935, regression from 9.20.1) (#3936) by @ndyakovrediscmd.AppendCmd redacts credential arguments (AUTH, HELLO ... AUTH, CONFIG SET secret params, ACL SETUSER password rules, MIGRATE AUTH/AUTH2), so redisotel / rediscensus span attributes no longer contain passwords (#3939) by @saddamr3eWaitAOF return type: returns *IntSliceCmd matching the two-integer WAITAOF reply (#3888) by @CipherN9Ring.Publish routing: publish to the shard that owns the topic instead of a round-robined one (#3893) by @dkindelOnRemove hooks: fire OnRemove on putConn eviction paths so removal hooks see every evicted connection (#3932) by @cxljsUniversalClient InfoMap: added InfoMap to the Cmdable interface (#3904) by @nazarli-shabnamSlowLogGet context: pass the caller's context instead of a background one (#3915) by @sonnemuskModuleLoadex nil config: return an error instead of panicking on nil config (#3916) by @sonnemuskParseURL IPv6 hosts: keep single brackets for IPv6 hosts without a port (#3882) by @sueun-devParseURL durations: treat unit durations <= 0 as disabled (#3866) by @sueun-dev*uint8 encoding: encode nil *uint8 as "0" like other numeric pointers (#3869) by @sueun-devJSONSliceCmd read errors: return the read error from readReply instead of swallowing it (#3903) by @saddamr3eReader.Discard (#3874) by @saddamr3e; reject unhashable keys in RESP3 map parsing (#3873) by @iabdullah215AddHook (#3868), onNewNode during Ring.SetAddrs (#3862), shared masters/slaves slices in cluster (#3861), shared opt.Addr during pub/sub reconnect (#3906), clusterStateReloadCallback in maintnotifications (#3894), conn reader in isHealthyConn during handoff (#3876) by @saddamr3e; handoff race window in maintnotifications (#3872) by @ndyakovredisotel: use ObservableCounter for cumulative pool stats (#3914) by @Solaris-star; avoid a data race on shared attributes during MinIdleConns warmup (#3881) by @ndyakovredislabs/client-libs-test:8.10.0 image and 8.8 was dropped from the CI matrix (#3940)sync/atomic value types (#3860) and remove the dead assertUnstableCommand RESP3 path (#3928) by @cxljsExpireTime / PExpireTime return Unix timestamps (#3917) by @sonnemusk; remove a duplicate example step (#3875) by @andy-stark-redisWe'd like to thank all the contributors who worked on this release!
@andy-stark-redis, @CipherN9, @cxljs, @dkindel, @iabdullah215, @nazarli-shabnam, @ndyakov, @ofekshenawa, @saddamr3e, @Solaris-star, @sonnemusk, @sueun-dev
Full Changelog: v9.21.0...v9.22.0
Nothing published for this version
This is a beta release adding support for Redis 8.10, new commands, and a large batch of stability and parser-robustness fixes. The 9.22.0 GA release
This is a beta release adding support for Redis 8.10, new commands, and a large batch of stability and parser-robustness fixes. The 9.22.0 GA release will follow once client-side caching and auto-pipelining are merged.
⚠️ Two changes to be aware of when upgrading from 9.21.0:
WaitAOF return type corrected (#3888): WaitAOF now returns *IntSliceCmd, matching the two-integer reply of WAITAOF (previously *IntCmd, which failed to parse the reply at runtime). Code referencing the old return type needs a one-line update.This release adds support for Redis 8.10. The README's supported-versions list now includes Redis 8.10, and CI runs the full suite against the redislabs/client-libs-test:8.10.0 image by default (#3920, #3940).
Coverage for the new commands and options that ship with Redis 8.10:
HIMPORT (#3919) — bulk hash import via server-side fieldsets, exposed as HImportPrepare, HImportSet, HImportDiscard, and HImportDiscardAll. Fieldsets are session state scoped to a single physical connection, which does not mix well with connection pooling — so the client keeps a versioned fieldset registry and lazily replays the PREPARE on whichever pooled connection executes a SET that needs it, at most once per connection, with no extra round trip (the PREPARE is injected into the same write as the SET).LMOVEM / BLMOVEM (#3913) — move multiple elements between lists in one call.SUNIONCARD / SDIFFCARD (#3897) — cardinality of set union/difference without materializing the result.XREAD / XREADGROUP MAXCOUNT and MAXSIZE (#3898) — bound how much data a stream read returns.TS.READ (#3896), TS.QUERYLABELS (#3926), TS.NRANGE / TS.NREVRANGE (#3870) with multiple aggregators per key (#3937), and EXCLUDEEMPTY on TS.MRANGE / TS.MREVRANGE (#3912) — new time-series query surface.FT.ALIASLIST (#3925), COLLECT reducer for FT.AGGREGATE (#3886), RERANK on HNSW vector fields in FT.CREATE (#3927), and FT.HYBRID timeout warnings (#3911) — search coverage.Default configuration values now follow the cross-SDK configuration proposal shared by all Redis client libraries (#3918):
| Setting | Old default | New default |
|---|---|---|
ReadTimeout / WriteTimeout |
3s | 5s |
| Retry backoff (min/max) | 8ms / 512ms | 10ms / 1s |
| Cluster state reload interval | 10s | 60s |
| TCP keep-alive | 5min period | 30s idle / 5s interval / 3 probes (net.KeepAliveConfig) |
Applications that set these values explicitly are unaffected; applications relying on the old defaults inherit the new ones.
A systematic audit fixed data races across the client — hooks (AddHook, #3868), Ring.SetAddrs (#3862), cluster node slices (#3861), pub/sub reconnect (#3906), maintenance notifications (#3894, #3872), pool handoff (#3876), and redisotel (#3881) — and hardened the RESP parsers against malformed or unexpected replies: over-reads on nil replies (#3874), integer overflow when skipping map/attribute bodies (#3877), unhashable RESP3 map keys (#3873), odd-length flat replies (#3900), mismatched declared array lengths (#3907), unexpected extra reply frames (#3884), and nil elements in numeric/bool slice replies (#3922).
Receive Hang FixPeekPushNotificationName blocked until 36 bytes were buffered, so a short subscribe confirmation (channel name of six or fewer characters) on an otherwise idle connection hung PubSub.Receive forever — a regression introduced in 9.20.1 by #3842. The peek now parses whatever is already buffered and only waits for one more byte when the frame prefix is valid but incomplete. Fixes #3935.
The cluster transaction pipeline treated a MULTI...EXEC block as independently retryable commands, which could scatter a transaction across nodes or send malformed transactions on retry. Redirects (MOVED/ASK/TRYAGAIN) and aborts are now handled at the whole-transaction level, matching Redis transaction semantics: the transaction is re-routed and retried as a unit, never partially (#3909) by @cxljs.
rediscmd.AppendCmd — used by redisotel and rediscensus to render commands into span attributes — now redacts credential arguments as <redacted>: AUTH, HELLO ... AUTH, CONFIG SET of requirepass / masterauth / TLS key passphrases, ACL SETUSER password rules, and MIGRATE ... AUTH/AUTH2. The client sends HELLO ... AUTH on every handshake and AUTH on every streaming-credentials rotation through the regular hook chain, so tracing hooks previously captured credentials even when the application never issued an auth command itself (#3939) by @saddamr3e.
HIMPORT command family: HImportPrepare / HImportSet / HImportDiscard / HImportDiscardAll with lazy per-connection fieldset prepare replay (#3919) by @ndyakovLMOVEM / BLMOVEM: move multiple list elements in one call, with COUNT (up to N) or EXACTLY (all-or-nothing) semantics via LMoveMArgs (#3913) by @ofekshenawaSUnionCard / SDiffCard: cardinality of set union/difference (#3897) by @ofekshenawaXRead / XReadGroup MAXCOUNT / MAXSIZE: bound stream read responses by entry count or payload size (#3898) by @ofekshenawaTS.READ: read samples from a series starting at a given timestamp, with TSReadEarliest (-), TSReadLatest (+), and TSReadNew ($) sentinels (#3896) by @ofekshenawaTS.QUERYLABELS: query label names/values across time series (#3926) by @ndyakovTS.NRANGE / TS.NREVRANGE: range queries across multiple series (#3870) by @ofekshenawa, with multiple aggregators per key (#3937) by @ndyakovTS.MRANGE / TS.MREVRANGE EXCLUDEEMPTY: skip series with no samples in the result (#3912) by @ofekshenawaFT.ALIASLIST: list all index aliases (#3925) by @ndyakovFT.AGGREGATE COLLECT reducer: collect grouped values into an array (#3886) by @ndyakovFT.CREATE RERANK: RERANK parameter on HNSW vector field definitions (#3927) by @ofekshenawaFT.HYBRID timeout warnings: timeout warnings are now populated in hybrid search results (#3911) by @ofekshenawaFT.HYBRID KNN SHARD_K_RATIO (Redis 8.8+): per-shard K ratio for KNN clauses (#3841) by @ndyakovReceive hang: peek push-notification names without demanding 36 buffered bytes, fixing a hang on short subscribe confirmations (fixes #3935, regression from 9.20.1) (#3936) by @ndyakovrediscmd.AppendCmd redacts credential arguments (AUTH, HELLO ... AUTH, CONFIG SET secret params, ACL SETUSER password rules, MIGRATE AUTH/AUTH2), so redisotel / rediscensus span attributes no longer contain passwords (#3939) by @saddamr3eWaitAOF return type: returns *IntSliceCmd matching the two-integer WAITAOF reply (#3888) by @CipherN9Ring.Publish routing: publish to the shard that owns the topic instead of a round-robined one (#3893) by @dkindelOnRemove hooks: fire OnRemove on putConn eviction paths so removal hooks see every evicted connection (#3932) by @cxljsUniversalClient InfoMap: added InfoMap to the Cmdable interface (#3904) by @nazarli-shabnamSlowLogGet context: pass the caller's context instead of a background one (#3915) by @sonnemuskModuleLoadex nil config: return an error instead of panicking on nil config (#3916) by @sonnemuskParseURL IPv6 hosts: keep single brackets for IPv6 hosts without a port (#3882) by @sueun-devParseURL durations: treat unit durations <= 0 as disabled (#3866) by @sueun-dev*uint8 encoding: encode nil *uint8 as "0" like other numeric pointers (#3869) by @sueun-devJSONSliceCmd read errors: return the read error from readReply instead of swallowing it (#3903) by @saddamr3eReader.Discard (#3874) by @saddamr3e; reject unhashable keys in RESP3 map parsing (#3873) by @iabdullah215AddHook (#3868), onNewNode during Ring.SetAddrs (#3862), shared masters/slaves slices in cluster (#3861), shared opt.Addr during pub/sub reconnect (#3906), clusterStateReloadCallback in maintnotifications (#3894), conn reader in isHealthyConn during handoff (#3876) by @saddamr3e; handoff race window in maintnotifications (#3872) by @ndyakovredisotel: use ObservableCounter for cumulative pool stats (#3914) by @Solaris-star; avoid a data race on shared attributes during MinIdleConns warmup (#3881) by @ndyakovredislabs/client-libs-test:8.10.0 image and 8.8 was dropped from the CI matrix (#3940)sync/atomic value types (#3860) and remove the dead assertUnstableCommand RESP3 path (#3928) by @cxljsExpireTime / PExpireTime return Unix timestamps (#3917) by @sonnemusk; remove a duplicate example step (#3875) by @andy-stark-redisWe'd like to thank all the contributors who worked on this release!
@andy-stark-redis, @CipherN9, @cxljs, @dkindel, @iabdullah215, @nazarli-shabnam, @ndyakov, @ofekshenawa, @saddamr3e, @Solaris-star, @sonnemusk, @sueun-dev
Full Changelog: https://github.com/redis/go-redis/compare/v9.21.0...v9.22.0-beta.1
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
This is a minor release adding new features and bug fixes. There are no breaking changes; upgrading from 9.20.x is a drop-in replacement.
This is a minor release adding new features and bug fixes. There are no breaking changes; upgrading from 9.20.x is a drop-in replacement.
GetToBuffer / SetFromBufferTwo new StringCmdable methods let callers read and write Redis string values directly into and from pre-allocated byte buffers, eliminating the per-call payload allocation that Get/Set incur:
GetToBuffer(ctx, key, buf) *ZeroCopyStringCmd // reads into buf; ZeroCopyStringCmd { Val() int; Bytes() []byte; Result() (int, error) }
SetFromBuffer(ctx, key, buf) *StatusCmd
GetToBuffer decodes the bulk reply straight into the caller-owned buf (no intermediate allocation); a buffer that is too small returns an error after draining the payload, so the connection stays aligned for the next reply. SetFromBuffer is provided for API symmetry — it dispatches to the same []byte writer path as Set(ctx, key, buf, 0) and produces byte-identical output on the wire. Available on *Client, *ClusterClient, *Ring, *Conn and Pipeliner.
LIMIT 0 for stream trimmingRedis treats XTRIM/XADD approximate-trim (~) LIMIT 0 as "disable the trimming effort cap entirely", which differs from omitting LIMIT (the implicit 100 * stream-node-max-entries default). The command builders previously only emitted LIMIT when limit > 0, so callers could never send an explicit LIMIT 0. Following the KeepTTL = -1 precedent, the new XTrimLimitDisabled = -1 sentinel now emits an explicit LIMIT 0; limit == 0 keeps the historical no-LIMIT behavior, so existing callers produce byte-identical commands.
(#3848) by @TheRealMal
GetToBuffer / SetFromBuffer on StringCmdable and the ZeroCopyStringCmd result type, reading/writing string values into caller-owned buffers without per-call payload allocation (#3834) by @ndyakovXTrimLimitDisabled sentinel: XTRIM/XADD approximate trimming can now send an explicit LIMIT 0 to disable the trim effort cap, via the new XTrimLimitDisabled = -1 sentinel (#3848) by @TheRealMalchannel.initHealthCheck now bounds the Ping it issues with a fresh per-check timeout context (the exported pingTimeout / reconnectTimeout) instead of context.TODO(), so a stuck health-check Ping can no longer block indefinitely (#3819) by @abdellaniUNWATCH in Tx.Close: a transaction now tracks whether a WATCH is still active (watchArmed) and only issues UNWATCH on Close when it is, removing an extra round trip on the common WATCH/.../EXEC and no-key Watch paths while never returning a connection to the pool with an active watch (#3854) by @fcostaoliveiramaintnotifications ModeAuto fail-open: ModeAuto now stays fail-open when the server does not support maintenance notifications — connections are retired and tracking is guarded during downgrade so the client keeps working instead of erroring (#3853) by @terrorobeWe'd like to thank all the contributors who worked on this release!
@abdellani, @fcostaoliveira, @ndyakov, @terrorobe, @TheRealMal
Full Changelog: https://github.com/redis/go-redis/compare/v9.20.1...v9.21.0
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
This is a patch release containing bug fixes only. There are no new features or breaking changes; upgrading from 9.20.0 is a drop-in replacement.
This is a patch release containing bug fixes only. There are no new features or breaking changes; upgrading from 9.20.0 is a drop-in replacement.
PeekPushNotificationName previously inspected only the bytes already buffered by bufio, so when a push frame header straddled a buffer fill boundary it could return a truncated notification name (e.g. "messa" instead of "message"). The push processor then mis-routed the frame and ReadReply silently dropped it, causing intermittent RESP3 pub/sub message loss. The peek now grows its window (36 bytes → up to 4 KiB) and reads more from the connection until the header is complete, cleanly separating incomplete prefixes from corrupt frames (including overflow-safe bulk-length handling). Fixes #3839.
PeekPushNotificationName no longer returns a truncated notification name when a push frame header spans a buffer boundary, preventing silent RESP3 pub/sub message loss (fixes #3839) (#3842) by @ndyakovFT.HYBRID vector params: Vector data is now always sent via PARAMS with auto-generated param names (__vector_param_N, with collision avoidance) when VectorParamName is omitted, since Redis no longer accepts inline vector blobs; the FTHybridOptions.Params map is no longer mutated, so the same options struct can be reused across calls (#3844) by @ndyakovCLUSTER SHARDS forward compatibility: Unknown shard- and node-level attributes in the CLUSTER SHARDS reply are now skipped via DiscardNext() instead of erroring, so clients keep working when the server introduces new fields (#3843) by @madolsonPubSub.releaseConn no longer reconnects twice when a connection is both unusable (or pending handoff) and reports a bad-connection error, avoiding a wasted connection establish-then-close cycle (#3833) by @cxljsWe'd like to thank all the contributors who worked on this release!
Full Changelog: https://github.com/redis/go-redis/compare/v9.20.0...v9.20.1
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →