go-redis/redis · error

redis: client is closed

Error message

redis: client is closed

What it means

pool.ErrClosed is returned by any pool/client operation attempted after Close() (pool.go:57-58). Once the client or pool is closed, Get/NewConn/Put and derived operations (pubsub open, etc.) fail immediately with this sentinel rather than attempting I/O.

Source

Thrown at internal/pool/pool.go:58

	CloseReasonMaintNotificationsDisabled = "maintnotifications_disabled"
)

// Metric state constants for connection state tracking.
// These represent the logical state of a connection from a metrics perspective,
// not the internal state machine state (ConnState).
const (
	// MetricStateIdle indicates the connection is idle in the pool,
	// ready to be acquired.
	MetricStateIdle = "idle"

	// MetricStateUsed indicates the connection is currently being used
	// by a client operation.
	MetricStateUsed = "used"
)

var (
	// ErrClosed performs any operation on the closed client will return this error.
	ErrClosed = errors.New("redis: client is closed")

	// ErrPoolExhausted is returned from a pool connection method
	// when the maximum number of database connections in the pool has been reached.
	ErrPoolExhausted = errors.New("redis: connection pool exhausted")

	// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
	ErrPoolTimeout = errors.New("redis: connection pool timeout")

	// ErrConnUnusableTimeout is returned when a connection is not usable and we timed out trying to mark it as unusable.
	ErrConnUnusableTimeout = errors.New("redis: timed out trying to mark connection as unusable")

	// errHookRequestedRemoval is returned when a hook requests connection removal.
	errHookRequestedRemoval = errors.New("hook requested removal")

	// errConnNotPooled is returned when trying to return a non-pooled connection to the pool.
	errConnNotPooled = errors.New("connection not pooled")

	// errConnEvictedIdle is passed to OnRemove hooks when a pooled connection is evicted on

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Do not reuse a client after Close(); create a new client for the new lifecycle.
  2. Coordinate close with a WaitGroup/context so in-flight ops finish first.
  3. Guard with client.(*redis.Client) and a closed flag, or check errors.Is(err, redis.ErrClosed).

Example fix

// before
client.Close()
client.Get(ctx, key) // ErrClosed
// after
client.Close()
client = nil
// on next use, lazily construct a new client
Defensive patterns

Strategy: validation

Validate before calling

// Track lifecycle yourself; go-redis has no public Closed() check.
// Don't reuse after Close(); recreate when needed.
if client == nil {
    client = redis.NewClient(opts)
}

Type guard

// There is no public Closed() accessor on *redis.Client.
// Track closure in your own wrapper.
type safeClient struct{ c *redis.Client; closed bool }

Try / catch

err := client.Get(ctx, key).Err()
if errors.Is(err, redis.ErrClosed) {
    // recreate the client for the new lifecycle
    client = redis.NewClient(client.Options())
}

Prevention

When it happens

Trigger: Calling client.Get/Set/Subscribe (or pool.Get) after client.Close() has been invoked; common in shutdown ordering bugs, reusing a client across goroutines where one closes it, or tests not resetting the client.

Common situations: Graceful-shutdown races, deferred Close() running before an in-flight op, or a long-lived shared client closed by one consumer.

Related errors


AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06). Data as JSON: /data/errors/00dd079cca1e1ed5.json. Report an issue: GitHub.