Repository: redis/go-redis

Issue:
Potential memory leaks on 9.12.0
Issue tracker is used for reporting bugs and discussing new features. Please use
[stackoverflow](https://stackoverflow.com) for supporting issues.

## Expected Behavior

I would not expect this steep increase of memory consumption.

## Current Behavior

Soon after it got deployed, ECS memory consumption got increased from 2% to 10%.
A Lamba function with 128mb of memory also failed several invocations.
Might not have enough data points since service deployment has been reverted, but looks like it's also gradually increasing.

<img width="493" height="330" alt="Image" src="https://github.com/user-attachments/assets/c6fc4e45-1e59-4965-a58c-f4e800d07c94" />

## Possible Solution

Don't have one.

## Steps to Reproduce

Deployed our service which handles high throughput of event streams.

## Context (Environment)

go 1.24.5
ECS: ARM64, debian:12-slim
Lambda: x86_64, provided.al2023

Event stream Lambda handlers got behind, backlogs built, ECS memory spike or leak didn't cause a problem since it was reverted.

<!--- Provide a general summary of the issue in the Title above -->

## Detailed Description

<!--- Provide a detailed description of the change or addition you are proposing -->

## Possible Implementation

<!--- Not obligatory, but suggest an idea for implementing addition or change -->


Discussion:
@ndyakov: Hello @onizmx, thank you for reporting this! Can you share which was the previous version you were using? @onizmx: @ndyakov Hi!
We were using 9.11.0 which I assume just a version before?@ndyakov: @onizmx Correct, I am just looking at the diff at the moment and trying to find the reason. One thing that looks promising is the new buffer size configurations: https://github.com/redis/go-redis?tab=readme-ov-file#buffer-size-configuration
Can you pleaase try by setting the buffer to a smaller size, previously the default as the default of go - 4096 https://github.com/golang/go/blob/master/src/bufio/bufio.go#L19 and now is 524288, which will make sense why you see the initial increase.

Feel free to drop it back to `4096` with the configuration and check if you observe any increase in the usage that look like potential memory leak. 
@onizmx: @ndyakov Thank you! I will be away for the next few days so will try it early next week and give you an update!
Just to make sure, these will be the configuration I will try:
```
	client := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:            []string{address},
		DisableIndentity: true,
		ReadBufferSize:   4096, <-- new
		WriteBufferSize:  4096, <-- new
	})
	readOnlyClient := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:            []string{address},
		ReadOnly:         true,
		RouteRandomly:    true,
		RouteByLatency:   true,
		DisableIndentity: true,
		ReadBufferSize:   4096, <-- new
		WriteBufferSize:  4096, <-- new
	})
```@ndyakov: Yes @onizmx, this should use the same size as in 9.11 , keep in mind bigger buffers can be beneficial for speed with bigger commands. You can play around with the size while testing. Please let me know when you have more data and we can decide how to proceed.@moritzploss-k: @ndyakov Is there a similar workaround in cases where the client is created using `NewFailoverClient(...)`? I don't see any settings for read/write buffer sizes in the `FailoverOptions` struct, and from the data I see so far, the memory usage keeps growing in my service after the upgrade to 9.12 (from 9.11).@ndyakov: @moritzploss-k you are absolutely right we may have missed the `FailoverOptions`. Let me fix that and i will release a patch version tomorrow. Also, from the feedback so far I will reduce the default size to half of it, how does this sound? @moritzploss-k: great, thanks for the quick fix! 🙌@cxljs: Hello, @ndyakov Have you started working on it? If not, I have time to fix it now.@ndyakov: @cxljs i have and it is ready, just updating the readme. Thank you for the readiness to work on that! @cxljs: Cool.@ndyakov: New version with lower size for the default is released, also includes failover options: 
https://github.com/redis/go-redis/releases/tag/v9.12.1

cc @moritzploss-k @onizmx: @ndyakov Thank you for looking into this!
I think what we are worrying about is not the memory allocation but any potential leaks.
I've vibe coded some memory testing / analysis here: https://github.com/onizmx/go-redis-memory (if you are interested) for various versions just to make sure that it's safe to deploy 9.12.1 to production.
We also have dev environment but unfortunately the load is drastically different which have failed us to catch the issue beforehand.
Anyways, I will try to deploy 9.12.1 tomorrow.

Just a quick glance of the test output:
<img width="2383" height="1769" alt="Image" src="https://github.com/user-attachments/assets/ae1d70ae-b6ca-4c05-bcf3-8caa626c68ba" />@ndyakov: @onizmx Hm, thanks. The GC Count is the only thing that would concern me looking at this graphic. Please share once you have more information and we can try to pinpoint if there are any potential commits included. BTW, please test with the previous default sizes of the buffers if you could. Thank you.

I will reopen this issue.
@onizmx: @ndyakov It's been around 2 hours after deploying our services with 9.12.1 and nothing sketchy so far. I will update again tomorrow!

```
	client := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:            []string{address},
		DisableIndentity: true,
		ReadBufferSize:   4096,
		WriteBufferSize:  4096,
	})
	readOnlyClient := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:            []string{address},
		ReadOnly:         true,
		RouteRandomly:    true,
		RouteByLatency:   true,
		DisableIndentity: true,
		ReadBufferSize:   4096,
		WriteBufferSize:  4096,
	})
```@ndyakov: Hey @onizmx, thanks for testing. If you could generate similar GC metric for when you are using the smaller buffer size that would be great. Let's see if there is something happening with the GC. I am thinking that maybe there is some go optimization to have less memory fragmentation and smaller buffers are collected more frequently? I don't know how else to explain it? @onizmx: @ndyakov Yeah looks like it. This time I tried with the buffer size params for 9.12.0 and 9.12.1, GC count is almost the same as 9.11.0.
Also, our service metrics are not showing any abnormal behaviour so happy for you to close the issue.
Thanks heaps again for the work!

<img width="2383" height="1769" alt="Image" src="https://github.com/user-attachments/assets/ae4cb3b8-1da4-4f11-b092-029dd175a4e8" />@ndyakov: @onizmx thank you for preparing the tool and reporting back. I'm closing the issue.@cxljs: Haha, maybe you meant to @onizmx @NgoKimPhu: looks like `UniversalOptions` was missed too...@cxljs: Thank you @NgoKimPhu , I fixed it in the PR #3485@PatrickDuncan: Hi, 9.12.0 caused our memory to increase by 5-10 times (With Redis v6.2.19). I think the default should go back to 4096 bytes with the next patch, but it should remain configurable. 9.12.1 uses about 2-3 times more memory than 9.11.x.

The README note doesn't seem enough.
>Important: If you experience any issues with the default buffer sizes, please try setting them to the go default of 4096 bytes.

Solution patch:
diff --git a/.github/wordlist.txt b/.github/wordlist.txt
index e0c73eb..741c51a 100644
--- a/.github/wordlist.txt
+++ b/.github/wordlist.txt
@@ -74,4 +74,5 @@ Azure
 StreamingCredentialsProvider
 oauth
 entraid
-MiB
\ No newline at end of file
+MiB
+KiB
diff --git a/README.md b/README.md
index 356870b..f4e73da 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ In `go-redis` we are aiming to support the last three releases of Redis. Current
 - [Redis 7.2](https://raw.githubusercontent.com/redis/redis/7.2/00-RELEASENOTES) - using Redis Stack 7.2 for modules support
 - [Redis 7.4](https://raw.githubusercontent.com/redis/redis/7.4/00-RELEASENOTES) - using Redis Stack 7.4 for modules support
 - [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 where modules are included
+- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2 where modules are included
 
 Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three
 versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0),
@@ -77,6 +78,7 @@ key value NoSQL database that uses RocksDB as storage engine and is compatible w
 - [Redis Ring](https://redis.uptrace.dev/guide/ring.html).
 - [Redis Performance Monitoring](https://redis.uptrace.dev/guide/redis-performance-monitoring.html).
 - [Redis Probabilistic [RedisStack]](https://redis.io/docs/data-types/probabilistic/)
+- [Customizable read and write buffers size.](#custom-buffer-sizes)
 
 ## Installation
 
@@ -372,6 +374,21 @@ For example:
 ```
 You can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/).
 
+#### Custom buffer sizes
+Prior to v9.12, the buffer size was the default go value of 4096 bytes. Starting from v9.12, 
+go-redis uses 256KiB read and write buffers by default for optimal performance.
+For high-throughput applications or large pipelines, you can customize buffer sizes:
+
+```go
+rdb := redis.NewClient(&redis.Options{
+    Addr:            "localhost:6379",
+    ReadBufferSize:  1024 * 1024, // 1MiB read buffer
+    WriteBufferSize: 1024 * 1024, // 1MiB write buffer
+})
+```
+
+**Important**: If you experience any issues with the default buffer sizes, please try setting them to the go default of 4096 bytes.
+
 ## Contributing
 We welcome contributions to the go-redis library! If you have a bug fix, feature request, or improvement, please open an issue or pull request on GitHub.
 We appreciate your help in making go-redis better for everyone.
@@ -412,6 +429,7 @@ vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello")
 res, err := rdb.Do(ctx, "set", "key", "value").Result()
 ```
 
+
 ## Run the test
 
 go-redis will start a redis-server and run the test cases.
diff --git a/internal/proto/reader.go b/internal/proto/reader.go
index a447809..13ff254 100644
--- a/internal/proto/reader.go
+++ b/internal/proto/reader.go
@@ -12,8 +12,8 @@ import (
 	"github.com/redis/go-redis/v9/internal/util"
 )
 
-// DefaultBufferSize is the default size for read/write buffers (0.5MiB)
-const DefaultBufferSize = 512 * 1024
+// DefaultBufferSize is the default size for read/write buffers (256 KiB).
+const DefaultBufferSize = 256 * 1024
 
 // redis resp protocol data type.
 const (
diff --git a/options.go b/options.go
index 2ce807e..6887b60 100644
--- a/options.go
+++ b/options.go
@@ -135,14 +135,14 @@ type Options struct {
 	// Larger buffers can improve performance for commands that return large responses.
 	// Smaller buffers can improve memory usage for larger pools.
 	//
-	// default: 0.5MiB (524288 bytes)
+	// default: 256KiB (262144 bytes)
 	ReadBufferSize int
 
 	// WriteBufferSize is the size of the bufio.Writer buffer for each connection.
 	// Larger buffers can improve performance for large pipelines and commands with many arguments.
 	// Smaller buffers can improve memory usage for larger pools.
 	//
-	// default: 0.5MiB (524288 bytes)
+	// default: 256KiB (262144 bytes)
 	WriteBufferSize int
 
 	// PoolFIFO type of connection pool.
diff --git a/osscluster.go b/osscluster.go
index 83817ca..0f678e6 100644
--- a/osscluster.go
+++ b/osscluster.go
@@ -96,14 +96,14 @@ type ClusterOptions struct {
 	// Larger buffers can improve performance for commands that return large responses.
 	// Smaller buffers can improve memory usage for larger pools.
 	//
-	// default: 0.5MiB (524288 bytes)
+	// default: 256KiB (262144 bytes)
 	ReadBufferSize int
 
 	// WriteBufferSize is the size of the bufio.Writer buffer for each connection.
 	// Larger buffers can improve performance for large pipelines and commands with many arguments.
 	// Smaller buffers can improve memory usage for larger pools.
 	//
-	// default: 0.5MiB (524288 bytes)
+	// default: 256KiB (262144 bytes)
 	WriteBufferSize int
 
 	TLSConfig *tls.Config
diff --git a/ring.go b/ring.go
index b0859b6..6c74031 100644
--- a/ring.go
+++ b/ring.go
@@ -128,14 +128,14 @@ type RingOptions struct {
 	// Larger buffers can improve performance for commands that return large responses.
 	// Smaller buffers can improve memory usage for larger pools.
 	//
-	// default: 0.5MiB (524288 bytes)
+	// default: 256KiB (262144 bytes)
 	ReadBufferSize int
 
 	// WriteBufferSize is the size of the bufio.Writer buffer for each connection.
 	// Larger buffers can improve performance for large pipelines and commands with many arguments.
 	// Smaller buffers can improve memory usage for larger pools.
 	//
-	// default: 0.5MiB (524288 bytes)
+	// default: 256KiB (262144 bytes)
 	WriteBufferSize int
 
 	TLSConfig *tls.Config
diff --git a/sentinel.go b/sentinel.go
index 9c90d5b..a858b08 100644
--- a/sentinel.go
+++ b/sentinel.go
@@ -90,6 +90,20 @@ type FailoverOptions struct {
 	WriteTimeout          time.Duration
 	ContextTimeoutEnabled bool
 
+	// ReadBufferSize is the size of the bufio.Reader buffer for each connection.
+	// Larger buffers can improve performance for commands that return large responses.
+	// Smaller buffers can improve memory usage for larger pools.
+	//
+	// default: 256KiB (262144 bytes)
+	ReadBufferSize int
+
+	// WriteBufferSize is the size of the bufio.Writer buffer for each connection.
+	// Larger buffers can improve performance for large pipelines and commands with many arguments.
+	// Smaller buffers can improve memory usage for larger pools.
+	//
+	// default: 256KiB (262144 bytes)
+	WriteBufferSize int
+
 	PoolFIFO bool
 
 	PoolSize        int
@@ -138,6 +152,9 @@ func (opt *FailoverOptions) clientOptions() *Options {
 		MinRetryBackoff: opt.MinRetryBackoff,
 		MaxRetryBackoff: opt.MaxRetryBackoff,
 
+		ReadBufferSize:  opt.ReadBufferSize,
+		WriteBufferSize: opt.WriteBufferSize,
+
 		DialTimeout:           opt.DialTimeout,
 		ReadTimeout:           opt.ReadTimeout,
 		WriteTimeout:          opt.WriteTimeout,
@@ -178,6 +195,9 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
 		MinRetryBackoff: opt.MinRetryBackoff,
 		MaxRetryBackoff: opt.MaxRetryBackoff,
 
+		ReadBufferSize:  opt.ReadBufferSize,
+		WriteBufferSize: opt.WriteBufferSize,
+
 		DialTimeout:           opt.DialTimeout,
 		ReadTimeout:           opt.ReadTimeout,
 		WriteTimeout:          opt.WriteTimeout,
@@ -224,6 +244,9 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
 		MinRetryBackoff: opt.MinRetryBackoff,
 		MaxRetryBackoff: opt.MaxRetryBackoff,
 
+		ReadBufferSize:  opt.ReadBufferSize,
+		WriteBufferSize: opt.WriteBufferSize,
+
 		DialTimeout:           opt.DialTimeout,
 		ReadTimeout:           opt.ReadTimeout,
 		WriteTimeout:          opt.WriteTimeout,



Test patch:
diff --git a/internal/pool/buffer_size_test.go b/internal/pool/buffer_size_test.go
index a542301..f2e4b30 100644
--- a/internal/pool/buffer_size_test.go
+++ b/internal/pool/buffer_size_test.go
@@ -34,12 +34,12 @@ var _ = Describe("Buffer Size Configuration", func() {
 		Expect(err).NotTo(HaveOccurred())
 		defer connPool.CloseConn(cn)
 
-		// Check that default buffer sizes are used (0.5MiB)
+		// Check that default buffer sizes are used (256KiB)
 		writerBufSize := getWriterBufSizeUnsafe(cn)
 		readerBufSize := getReaderBufSizeUnsafe(cn)
 
-		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
-		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
+		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
+		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
 	})
 
 	It("should use custom buffer sizes when specified", func() {
@@ -79,16 +79,16 @@ var _ = Describe("Buffer Size Configuration", func() {
 		Expect(err).NotTo(HaveOccurred())
 		defer connPool.CloseConn(cn)
 
-		// Check that default buffer sizes are used (0.5MiB)
+		// Check that default buffer sizes are used (256KiB)
 		writerBufSize := getWriterBufSizeUnsafe(cn)
 		readerBufSize := getReaderBufSizeUnsafe(cn)
 
-		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
-		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
+		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
+		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
 	})
 
-	It("should use 0.5MiB default buffer sizes for standalone NewConn", func() {
-		// Test that NewConn (without pool) also uses 0.5MiB defaults
+	It("should use 256KiB default buffer sizes for standalone NewConn", func() {
+		// Test that NewConn (without pool) also uses 256KiB buffers
 		netConn := newDummyConn()
 		cn := pool.NewConn(netConn)
 		defer cn.Close()
@@ -96,11 +96,11 @@ var _ = Describe("Buffer Size Configuration", func() {
 		writerBufSize := getWriterBufSizeUnsafe(cn)
 		readerBufSize := getReaderBufSizeUnsafe(cn)
 
-		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
-		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
+		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
+		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
 	})
 
-	It("should use 0.5MiB defaults even when pool is created directly without buffer sizes", func() {
+	It("should use 256KiB defaults even when pool is created directly without buffer sizes", func() {
 		// Test the scenario where someone creates a pool directly (like in tests)
 		// without setting ReadBufferSize and WriteBufferSize
 		connPool = pool.NewConnPool(&pool.Options{
@@ -114,12 +114,12 @@ var _ = Describe("Buffer Size Configuration", func() {
 		Expect(err).NotTo(HaveOccurred())
 		defer connPool.CloseConn(cn)
 
-		// Should still get 0.5MiB defaults because NewConnPool sets them
+		// Should still get 256KiB defaults because NewConnPool sets them
 		writerBufSize := getWriterBufSizeUnsafe(cn)
 		readerBufSize := getReaderBufSizeUnsafe(cn)
 
-		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
-		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size
+		Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
+		Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 256KiB buffer size
 	})
 })