go-redis/redis · warning

redis: timed out trying to mark connection as unusable

Error message

redis: timed out trying to mark connection as unusable

What it means

pool.ErrConnUnusableTimeout is returned when a connection was found unusable but the pool could not transition/remove it within the wait budget (pool.go:67-68). It indicates a race during connection removal under pressure — the pool timed out trying to mark the connection as unusable and instead surfaces this sentinel.

Source

Thrown at internal/pool/pool.go:68

	// 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
	// Put because the idle pool is already at MaxIdleConns.
	errConnEvictedIdle = errors.New("connection evicted: idle pool at capacity")

	// metricCallbackMu protects all global metric callback functions for thread-safe access.
	metricCallbackMu sync.RWMutex

	// Global metric callbacks for connection state changes
	metricConnectionStateChangeCallback func(ctx context.Context, cn *Conn, fromState, toState string)

	// Global metric callback for connection creation time

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Treat as transient and retry the operation; the connection will be gone on the next attempt.
  2. Reduce pool churn — raise PoolSize/MinIdleConns so fewer emergency dials/removals happen.
  3. Check OnGet/OnPut hooks aren't unconditionally requesting removal.

Example fix

// before
return client.Get(ctx, key).Err()
// after
for i := 0; i < 3; i++ {
    cmd := client.Get(ctx, key)
    if err := cmd.Err(); err != nil {
        if errors.Is(err, redis.ErrClosed) { break }
        if isBadConn(err, false) { continue }
        return err
    }
    return nil
}
Defensive patterns

Strategy: retry

Validate before calling

// Transient removal race — no deterministic pre-check.
// Reduce churn via adequate PoolSize/MinIdleConns and sane hooks.

Try / catch

for i := 0; i < 3; i++ {
    err := client.Get(ctx, key).Err()
    if err == nil { break }
    if isBadConn(err, false) { continue }
    return err
}

Prevention

When it happens

Trigger: High-churn pool teardown interleaved with concurrent Get/Put, or maintnotifications forcibly removing connections while the pool is contended.

Common situations: Maintenance windows with heavy load, frequent connection eviction, or a custom OnGet/OnPut hook requesting removal under saturation.

Related errors


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