From 2633b26647b5f9e66a7b2aee0bb798c45b3ca55a Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 25 Aug 2026 12:03:12 +0100 Subject: [PATCH 1/2] DOC-6996 Document the go-redis pipeline connection pool Add a Connection pooling section to the go-redis production usage page covering the dedicated pipeline pool that go-redis PR #3959 makes default-on, and point the two pipeline pages at it. The ticket framed this as expanding the autopipeline paragraph at lines 157-160. That turned out to be the wrong home. Pipeline() and TxPipeline() both route through withPipelineConn (redis.go:1753-1765, 1807), so the new pool serves hand-built pipelines and MULTI/EXEC too, not just automatic pipelining -- and autopipeline.md carries an "experimental feature" banner that would have wrongly colored default-on pooling behavior. produsage.md already owns Timeouts and Retries, so sizing advice belongs there; the Go guide had no pooling prose at all before this. The ticket's "Not parked: the change is already merged upstream" is a merged-vs-released conflation. v9.22.0 shipped 2026-08-03, #3959 merged 2026-08-24, master is 15 commits ahead of the tag and no release contains it. Ran the negative check to be sure rather than reasoning from dates: on v9.22.0, PoolStats().PipelineStats is nil by default AND stays nil with PipelinePoolSize:10 alone, because the released version only builds the pool when a buffer field is set. So the whole section describes behavior no reader can observe yet, and the version line is a deliberate vX.Y.Z placeholder with an HTML TODO. Compiling the PipelineStats snippet was necessary but not sufficient. Running it against master + Redis 8.8 is what confirmed the claims: pipeline pool non-nil and holding zero connections when idle, a plain command touching only the main pool, both Pipeline() and TxPipeline() using the pipeline pool, PipelinePoolSize:-1 making PipelineStats nil, and 60 concurrent pipelines capping the pool at 10. The fallback claim did NOT reproduce on that burst (Timeouts stayed 0, main pool untouched) -- fast batches never hold a connection past the 100ms wait. It took PipelinePoolSize:1 plus BLPOP batches to force it: Timeouts=5 and five batches on the main pool. DEBUG SLEEP is unavailable on the local server, so BLPOP on a never-populated key is the way to hold a pipeline connection open. Also fixed a pre-existing broken anchor in the same checklist: #seamless-client-experience never matched the "Smart client handoffs" heading. Deliberately not done: no connect.md change for client-side caching. Pipeline connections now skip CLIENT TRACKING (redis.go:942-949), but pipelined commands never consulted or populated the cache in v9.22.0 either, so nothing a reader can observe changed. URL query params deferred by decision -- documenting them drags in ParseURL coverage the Go guide has never had. Upstream doc bug worth reporting: osscluster.go:148 still says the pool is created "only when PipelineReadBufferSize or PipelineWriteBufferSize is set", stale after #3959 and contradicting ring.go:154. Cluster nodes are built via clOpt.NewClient() (osscluster.go:518) with PipelinePoolSize passed through, so they do get pipeline pools -- which is also why the ceiling arithmetic multiplies per node. Learned: merged != released, and a saturation claim needs saturation forced -- a 60-pipeline burst never triggered the main-pool fallback that PipelinePoolSize:1 plus BLPOP did. Constraint: the PipelineStats snippet must keep `if ps := stats.PipelineStats; ps != nil` -- the field is *internal/pool.Stats, so type inference is the only form that compiles outside the module, not a style preference. Constraint: produsage.md carries no page-level bannerText; the pipeline pool's version requirement is stated in-section so the released Health checks, Retries and Timeouts sections are not mislabeled as unreleased. Rejected: expanding autopipeline.md lines 157-160 as the ticket suggested | files default-on pooling under that page's experimental-feature banner and leaves hand-built Pipeline()/TxPipeline() readers with nothing Gaps: the 64 KiB buffer defaults and the RESP3 minimum clamp are read from pipelinePoolOptions, not observed at runtime; the Limiter-charged-once behavior is likewise source-only. Recheck: replace the vX.Y.Z placeholder and delete the DOC-6996 HTML comment in produsage.md when a non-prerelease go-redis tag contains bd0cea4. Directive: verify DefaultPipelinePoolSize (10), DefaultPipelineBufferSize (64 KiB) and DefaultPipelinePoolTimeout (100ms) against the shipping tag before merging -- all three are quoted as bare numbers in the prose and table. Ticket: DOC-6996 Co-Authored-By: Claude Opus 5 (1M context) --- content/develop/clients/go/autopipeline.md | 8 +-- content/develop/clients/go/produsage.md | 66 +++++++++++++++++++++- content/develop/clients/go/transpipe.md | 6 ++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/content/develop/clients/go/autopipeline.md b/content/develop/clients/go/autopipeline.md index 274a4b22e0..69b2d376d3 100644 --- a/content/develop/clients/go/autopipeline.md +++ b/content/develop/clients/go/autopipeline.md @@ -154,10 +154,10 @@ commands instead of 200. As soon as you supply `AutoPipelineOptions`, either on the client or to `AutoPipelineWithOptions()`, that preset no longer applies, and a `MaxBatchSize` you leave unset means 200. -Connection and buffer tuning is not part of `AutoPipelineOptions`. Batches use -the client's pipeline connections, which you size with the -`PipelineReadBufferSize`, `PipelineWriteBufferSize`, and `PipelinePoolSize` -fields of the client's options. +Connection and buffer tuning is not part of `AutoPipelineOptions`. Batches run +on the client's separate pipeline connection pool, which you size with the +fields described in +[Connection pooling]({{< relref "/develop/clients/go/produsage#connection-pooling" >}}). Each client holds at most two autopipeliners: one for the blocking method and one for the asynchronous method. Each of them is a diff --git a/content/develop/clients/go/produsage.md b/content/develop/clients/go/produsage.md index 687da074f6..1a0af30a12 100644 --- a/content/develop/clients/go/produsage.md +++ b/content/develop/clients/go/produsage.md @@ -30,7 +30,8 @@ progress in implementing the recommendations. - [ ] [Monitor performance and errors](#monitor-performance-and-errors) - [ ] [Retries](#retries) - [ ] [Timeouts](#timeouts) -- [ ] [Smart client handoffs](#seamless-client-experience) +- [ ] [Connection pooling](#connection-pooling) +- [ ] [Smart client handoffs](#smart-client-handoffs) ``` ## Recommendations @@ -127,6 +128,69 @@ might retry commands that would have succeeded if given more time. However, if they are too long, your app might hang unnecessarily while waiting for a response that will never arrive. +### Connection pooling + +`go-redis` manages connections for you with a +[connection pool]({{< relref "/develop/clients/pools-and-muxing" >}}), so your +app doesn't have to cache and reuse open connections itself. The `PoolSize` +field of `Options` sets the number of connections the main pool keeps, which +defaults to ten times the value of `GOMAXPROCS`. `MinIdleConns` sets how many +connections to open before your app asks for them, `MaxActiveConns` caps the +total number of connections, and `PoolTimeout` sets how long a command waits +for a free connection. By default, no connections are opened in advance, the +total is uncapped, and a command waits one second longer than `ReadTimeout`. + +Pipelines don't use the main pool. Every client also creates a separate +*pipeline pool* that +[pipelines and transactions]({{< relref "/develop/clients/go/transpipe" >}}) and +[automatic pipelining]({{< relref "/develop/clients/go/autopipeline" >}}) use +for each batch they run. This keeps a burst of batches from competing with your +ordinary commands for the same connections. You don't have to enable it. + +The pipeline pool is sized with these fields of `Options`: + +| Field | Description | +| :---- | :---------- | +| `PipelinePoolSize` | Number of connections the pipeline pool keeps. Defaults to 10. Set it to `-1` to do without a pipeline pool, which returns pipelines to the main pool. | +| `PipelineReadBufferSize` | Size of the read buffer for each pipeline connection. Defaults to the larger of `ReadBufferSize` and 64 KiB, because a batch brings back many replies in a single round trip. A value smaller than the minimum that the [RESP3]({{< relref "/develop/reference/protocol-spec#resp-versions" >}}) protocol needs for push messages is raised to that minimum. | +| `PipelineWriteBufferSize` | Size of the write buffer for each pipeline connection. Defaults to the larger of `WriteBufferSize` and 64 KiB. | + +The pipeline pool costs you nothing while you aren't pipelining. It never opens +connections in advance, whatever `MinIdleConns` you set, so an idle pipeline +pool holds no connections at all. It also doesn't take its cap from +`MaxActiveConns`, which would double the number of connections your client can +open. The limit is instead `MaxActiveConns` plus `PipelinePoolSize`. +`ClusterClient` and `Ring` create a pipeline pool for each node, so count that +addition once per node rather than once per client. + +When every pipeline connection is busy, a batch waits 100 milliseconds and then +runs on the main pool rather than queuing for a pipeline connection. A +`PoolTimeout` shorter than 100 milliseconds shortens that wait as well. The main +pool still applies `MaxActiveConns` to a batch that reaches it this way, and a +`Limiter` counts such a batch once rather than twice. + +To find out whether the pipeline pool is the right size, read `PipelineStats` +from the client's pool statistics: + +```go +stats := client.PoolStats() +fmt.Printf("Main pool hits: %d, misses: %d, timeouts: %d\n", + stats.Hits, stats.Misses, stats.Timeouts) +if ps := stats.PipelineStats; ps != nil { + fmt.Printf("Pipeline pool connections: %d, hits: %d, timeouts: %d\n", + ps.TotalConns, ps.Hits, ps.Timeouts) +} +``` + +A growing `Timeouts` count on the pipeline pool means batches are waiting for a +connection and then running on the main pool, so try a larger +`PipelinePoolSize`. +`PipelineStats` is `nil` when you set `PipelinePoolSize` to `-1`, and combines +the figures from every node for `ClusterClient` and `Ring`. + +The pipeline pool requires `github.com/redis/go-redis/v9` vX.Y.Z or later. + + ### Smart client handoffs *Smart client handoffs (SCH)* is a feature of Redis Cloud and diff --git a/content/develop/clients/go/transpipe.md b/content/develop/clients/go/transpipe.md index 4cefc3c299..f3e2bdf0a7 100644 --- a/content/develop/clients/go/transpipe.md +++ b/content/develop/clients/go/transpipe.md @@ -32,6 +32,12 @@ If you want the client to batch concurrent commands into pipelines for you without writing any pipeline code, see [Automatic pipelining]({{< relref "/develop/clients/go/autopipeline" >}}). +Both types of batch run on a separate pool of connections that the client keeps +for them, so a burst of batches doesn't compete with your ordinary commands for +the same connections. See +[Connection pooling]({{< relref "/develop/clients/go/produsage#connection-pooling" >}}) +for how to size that pool. + ## Execute a pipeline To execute commands in a pipeline, you first create a pipeline object From 48490fec351a23cfc932903ac0b6a8b31f78f61d Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 25 Aug 2026 14:33:43 +0100 Subject: [PATCH 2/2] DOC-6996 tidied up Claude's initial attempt --- content/develop/clients/go/produsage.md | 63 +++++++++++++------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/content/develop/clients/go/produsage.md b/content/develop/clients/go/produsage.md index 1a0af30a12..5f7d49440f 100644 --- a/content/develop/clients/go/produsage.md +++ b/content/develop/clients/go/produsage.md @@ -133,44 +133,47 @@ response that will never arrive. `go-redis` manages connections for you with a [connection pool]({{< relref "/develop/clients/pools-and-muxing" >}}), so your app doesn't have to cache and reuse open connections itself. The `PoolSize` -field of `Options` sets the number of connections the main pool keeps, which -defaults to ten times the value of `GOMAXPROCS`. `MinIdleConns` sets how many +field of `Options` sets the number of +connections the main pool keeps (with a default of ten times the value of `GOMAXPROCS`). `MinIdleConns` sets how many connections to open before your app asks for them, `MaxActiveConns` caps the total number of connections, and `PoolTimeout` sets how long a command waits for a free connection. By default, no connections are opened in advance, the total is uncapped, and a command waits one second longer than `ReadTimeout`. -Pipelines don't use the main pool. Every client also creates a separate -*pipeline pool* that +By default, pipelines don't use the main pool. Every client also creates a +separate *pipeline pool* that [pipelines and transactions]({{< relref "/develop/clients/go/transpipe" >}}) and [automatic pipelining]({{< relref "/develop/clients/go/autopipeline" >}}) use -for each batch they run. This keeps a burst of batches from competing with your -ordinary commands for the same connections. You don't have to enable it. +for each batch they run. This prevents a burst of batches from competing with your +ordinary commands for the same connections. -The pipeline pool is sized with these fields of `Options`: +{{< note >}}The pipeline pool is available in `github.com/redis/go-redis/v9` vX.Y.Z or later. + +{{< / note >}} +Use the following fields of `Options` to size the pipeline pool: | Field | Description | | :---- | :---------- | -| `PipelinePoolSize` | Number of connections the pipeline pool keeps. Defaults to 10. Set it to `-1` to do without a pipeline pool, which returns pipelines to the main pool. | -| `PipelineReadBufferSize` | Size of the read buffer for each pipeline connection. Defaults to the larger of `ReadBufferSize` and 64 KiB, because a batch brings back many replies in a single round trip. A value smaller than the minimum that the [RESP3]({{< relref "/develop/reference/protocol-spec#resp-versions" >}}) protocol needs for push messages is raised to that minimum. | +| `PipelinePoolSize` | Number of connections the pipeline pool keeps. Defaults to 10. Set it to `0` to use the default number of connections, or to `-1` to disable the separate pipeline pool (which means connections are allocated to pipelines from the main pool). | +| `PipelineReadBufferSize` | Size of the read buffer for each pipeline connection. Defaults to the larger of `ReadBufferSize` and 64 KiB, because a batch of commands can return many replies in a single round trip. If the value is set smaller than the minimum required by the [RESP3]({{< relref "/develop/reference/protocol-spec#resp-versions" >}}) protocol for push messages then that minimum is used instead. | | `PipelineWriteBufferSize` | Size of the write buffer for each pipeline connection. Defaults to the larger of `WriteBufferSize` and 64 KiB. | -The pipeline pool costs you nothing while you aren't pipelining. It never opens -connections in advance, whatever `MinIdleConns` you set, so an idle pipeline -pool holds no connections at all. It also doesn't take its cap from -`MaxActiveConns`, which would double the number of connections your client can -open. The limit is instead `MaxActiveConns` plus `PipelinePoolSize`. -`ClusterClient` and `Ring` create a pipeline pool for each node, so count that -addition once per node rather than once per client. +The pipeline pool costs you nothing while you are not using pipelining. It never opens +connections in advance regardless of the value of `MinIdleConns`, so an idle pipeline +pool holds no connections at all. Also, it doesn't use +`MaxActiveConns` as its upper limit (which would double the number of connections your client can +open). The limit is instead the value of `MaxActiveConns` plus `PipelinePoolSize`. +`ClusterClient` and `Ring` create a pipeline pool for each node, so this limit +applies for each node rather than for each client. -When every pipeline connection is busy, a batch waits 100 milliseconds and then -runs on the main pool rather than queuing for a pipeline connection. A -`PoolTimeout` shorter than 100 milliseconds shortens that wait as well. The main -pool still applies `MaxActiveConns` to a batch that reaches it this way, and a -`Limiter` counts such a batch once rather than twice. +If every pipeline connection is busy, a batch will wait for the number of milliseconds +specified in `PoolTimeout` (up to a maximum of 100 milliseconds) and then +run on the main pool rather than queuing for a pipeline connection. The main +pool still applies the `MaxActiveConns` limit when a batch is allocated a connection in + this way, and a `Limiter` counts such a batch once rather than twice. -To find out whether the pipeline pool is the right size, read `PipelineStats` -from the client's pool statistics: +You can find out the current size of the pipeline pool using the `PipelineStats` +field from the client's pool statistics: ```go stats := client.PoolStats() @@ -182,14 +185,12 @@ if ps := stats.PipelineStats; ps != nil { } ``` -A growing `Timeouts` count on the pipeline pool means batches are waiting for a -connection and then running on the main pool, so try a larger -`PipelinePoolSize`. -`PipelineStats` is `nil` when you set `PipelinePoolSize` to `-1`, and combines -the figures from every node for `ClusterClient` and `Ring`. - -The pipeline pool requires `github.com/redis/go-redis/v9` vX.Y.Z or later. - +The `PipelineStats.Timeouts` field indicates how many pipelines have timed out while +waiting for a connection and have consequently run on the main pool. Increase +`PipelinePoolSize` if the number of timeouts is growing rapidly. +Note that `PipelineStats` is `nil` when you disable the pipeline pool (by setting +`PipelinePoolSize` to `-1`). It also combines +the figures from every node for `ClusterClient` and `Ring` into the same count. ### Smart client handoffs