go-redis/redis · error

max handoff retries reached

Error message

max handoff retries reached

What it means

Returned by performHandoffInternal (handoff_worker.go:407-412) when IncrementAndGetHandoffRetries exceeds Config.MaxHandoffRetries. Unlike other handoff errors this one is non-retryable (shouldRetry=false): the connection has failed to relocate to its new endpoint too many times and is removed from the pool rather than being queued again.

Source

Thrown at maintnotifications/errors.go:17

package maintnotifications

import (
	"errors"

	"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
)

// Configuration errors
var (
	ErrInvalidRelaxedTimeout             = errors.New(logs.InvalidRelaxedTimeoutError())
	ErrInvalidHandoffTimeout             = errors.New(logs.InvalidHandoffTimeoutError())
	ErrInvalidHandoffWorkers             = errors.New(logs.InvalidHandoffWorkersError())
	ErrInvalidHandoffQueueSize           = errors.New(logs.InvalidHandoffQueueSizeError())
	ErrInvalidPostHandoffRelaxedDuration = errors.New(logs.InvalidPostHandoffRelaxedDurationError())
	ErrInvalidEndpointType               = errors.New(logs.InvalidEndpointTypeError())
	ErrInvalidMaintNotifications         = errors.New(logs.InvalidMaintNotificationsError())
	ErrMaxHandoffRetriesReached          = errors.New(logs.MaxHandoffRetriesReachedError())

	// Configuration validation errors

	// ErrInvalidHandoffRetries is returned when the number of handoff retries is invalid
	ErrInvalidHandoffRetries = errors.New(logs.InvalidHandoffRetriesError())
)

// Integration errors
var (
	// ErrInvalidClient is returned when the client does not support push notifications
	ErrInvalidClient = errors.New(logs.InvalidClientError())
)

// Handoff errors
var (
	// ErrHandoffQueueFull is returned when the handoff queue is full
	ErrHandoffQueueFull = errors.New(logs.HandoffQueueFullError())

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Verify the new endpoint is reachable from the client host (telnet/curl the host:port the MOVING notification advertised).
  2. Check EndpointType: if the server returns an internal IP/FQDN the client cannot route to, switch to an external type or fix network/DNS.
  3. If the endpoint is genuinely flaky but recoverable, raise Config.MaxHandoffRetries (1-10) to give handoff more attempts.
  4. Increase HandoffTimeout so transient slow dials don't each burn a retry.

Example fix

// before
cfg := maintnotifications.DefaultConfig() // MaxHandoffRetries: 3

// after
cfg := maintnotifications.DefaultConfig()
cfg.MaxHandoffRetries = 5
cfg.HandoffTimeout = 30 * time.Second
cfg.EndpointType = maintnotifications.EndpointTypeExternalIP
Defensive patterns

Strategy: retry

Try / catch

if errors.Is(err, maintnotifications.ErrMaxHandoffRetriesReached) {
    // connection was dropped; the next command will use a fresh pooled conn.
    // Investigate endpoint reachability / EndpointType, then retry the command.
}

Prevention

When it happens

Trigger: A connection receives a MOVING notification, is handed off, but the new endpoint is repeatedly unreachable or the dial keeps failing. Each retry increments the counter; once it crosses MaxHandoffRetries (default 3), performConnectionHandoff returns ErrMaxHandoffRetriesReached and processHandoffRequest closes the connection (handoff_worker.go:247-251).

Common situations: The new endpoint returned by MOVING is wrong/unreachable (firewall, DNS, wrong EndpointType for the network); the target node is down during a multi-shard migration storm; HandoffTimeout too short causing repeated dial aborts that each count as a retry.

Related errors


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