go-redis/redis · error

failed to subscribe to streaming credentials: %w

Error message

failed to subscribe to streaming credentials: %w

What it means

Thrown during per-connection initialization when Options.StreamingCredentialsProvider.Subscribe(listener) returns an error. Every new/reinitialized pool connection subscribes a listener so the client can re-AUTH on token rotation; if the provider refuses the subscription (e.g. it is closed or the identity backend is unreachable), the connection is transitioned to StateClosed and the error is wrapped and surfaced through the dialer.

Source

Thrown at redis.go:753

	conn.baseClient.allowClientTracking = true

	username, password := "", ""
	if c.opt.StreamingCredentialsProvider != nil {
		credListener, initErr := c.streamingCredentialsManager.Listener(
			cn,
			c.reAuthConnection(),
			c.onAuthenticationErr(),
		)
		if initErr != nil {
			cn.GetStateMachine().Transition(pool.StateClosed)
			return fmt.Errorf("failed to create credentials listener: %w", initErr)
		}

		credentials, unsubscribeFromCredentialsProvider, initErr := c.opt.StreamingCredentialsProvider.
			Subscribe(credListener)
		if initErr != nil {
			cn.GetStateMachine().Transition(pool.StateClosed)
			return fmt.Errorf("failed to subscribe to streaming credentials: %w", initErr)
		}

		// Per-connection unsubscribe is attached to the connection itself so it
		// runs when this specific connection is closed. Do not register it on
		// c.onClose: initConn runs for every (re)initialized connection, and
		// attaching per-connection state to the shared baseClient registry would
		// either leak entries (one per connection id, never trimmed) or — with
		// the pre-fix wrappedOnClose approach — build an unbounded closure chain
		// retaining every prior connection's unsubscribe (see issue #3772).
		//
		// Note: pool.Conn.SetOnClose OVERWRITES any prior callback (see the
		// doc on that method). That is safe here because the streaming
		// credentials Manager deduplicates listeners by connection id, so a
		// second initConn on the same cn re-Subscribes the SAME listener and
		// the returned unsubscribe is equivalent to the one already installed.
		// Any future code path that could hand out a distinct unsubscribe on
		// re-initialization must first invoke the existing one to avoid
		// orphaning the old subscription on the credentials provider.

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Verify the streaming credentials provider is still open and was constructed with valid IdP settings (tenant, client ID, secret/scope) before the client is used.
  2. Check network connectivity from the app host to the identity provider token endpoint (TLS handshake, proxy env vars, DNS).
  3. If streaming credentials are optional, remove StreamingCredentialsProvider and fall back to static Username/Password or a context/function provider until the IdP issue is resolved.
  4. Inspect the wrapped error (%w) for the underlying cause; provider implementations usually expose a typed error you can branch on.

Example fix

// before
provider := entraid.NewProvider(entraid.Config{TenantID: tenant, ClientID: cid})
opt.StreamingCredentialsProvider = provider
client := redis.NewClient(opt)
// provider.Subscribe fails because the secret was never set

// after
provider, err := entraid.NewProvider(entraid.Config{
    TenantID: tenant, ClientID: cid, ClientSecret: secret,
})
if err != nil { return err }
opt.StreamingCredentialsProvider = provider
client := redis.NewClient(opt)
Defensive patterns

Strategy: validation

Validate before calling

// Validate the streaming provider can subscribe before constructing the client.
// Most providers expose a Health/Ping or a dry-run Subscribe you can probe.
if p, ok := opt.StreamingCredentialsProvider.(interface{ Healthy() bool }); ok && !p.Healthy() {
    return errors.New("streaming credentials provider is not healthy")
}

Try / catch

// Wrap the first command so dial/init errors surface as normal Go errors.
if err := client.Ping(ctx).Err(); err != nil {
    var se *streaming.SubscribeError // provider-specific typed error if exposed
    if errors.As(err, &se) {
        // handle provider-side failure (recreate provider, fall back)
    }
    return err
}

Prevention

When it happens

Trigger: Setting Options.StreamingCredentialsProvider (e.g. the Entra ID provider from go-redis-entraid) and then performing any operation that dials a connection, while the provider's Subscribe() returns a non-nil error. Also triggered on reconnect/reinit since initConn runs for every connection.

Common situations: Streaming credentials provider was Close()d before the client issued commands; misconfigured tenant/client ID/secret against the identity provider; the IdP token endpoint is unreachable (DNS, firewall, proxy); provider initialized with an expired/invalid cache; network partition between the app and the OAuth issuer.

Related errors


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