redis/redis-rb · error · Redis::SubscriptionError

This client is already subscribed

Error message

This client is already subscribed

What it means

One Redis instance supports at most one active subscription. _subscription raises Redis::SubscriptionError 'This client is already subscribed' when a new subscribe/psubscribe/subscribe_with_timeout block starts while @subscription_client is still set — i.e. a previous subscription loop on this same instance has not finished (the block form sets it for the loop's duration and clears it in an ensure).

Solutions

  1. Give each concurrent subscription its own Redis instance (Redis.new with the same URL)
  2. Subscribe once to all channels in a single call — the block receives messages from every channel
  3. Sequence subscriptions on one client: wait for the previous block to return (it exits after unsubscribe/timeout) before starting the next

Example fix

# before
redis.subscribe('news') do |on|
  on.message { |_, msg| redis.subscribe('alerts') { } }  # nested: raises already subscribed
end

# after
redis.subscribe('news', 'alerts') do |on|
  on.message { |channel, msg| handle(channel, msg) }
end
Defensive patterns

Strategy: validation

Validate before calling

SUB_LOCK = Mutex.new

def subscribe_exclusive(redis, *channels, &block)
  raise Redis::SubscriptionError, 'another subscription is active' unless SUB_LOCK.try_lock
  redis.subscribe(*channels, &block)
ensure
  SUB_LOCK.unlock if SUB_LOCK.owned?
end

Try / catch

begin
  redis.subscribe('ch') { |on| on.message { |_, m| handle(m) } }
rescue Redis::SubscriptionError
  subscriber = Redis.new(url: redis.connection[:host] ? config_url : config_url)
  subscriber.subscribe('ch') { |on| on.message { |_, m| handle(m) } }
end

Prevention

When it happens

Trigger: Calling redis.subscribe (or psubscribe, subscribe_with_timeout, ssubscribe) from inside another subscription block on the same instance, or from a second thread while a subscription loop is still running on the same Redis object.

Common situations: A background thread runs redis.subscribe { ... } while the main thread subscribes on the same shared client; pub/sub fan-out code that starts overlapping subscriptions; Rails/Sidekiq apps sharing one global Redis constant for both commands and subscriptions.

Related errors


AI-assisted analysis of redis/redis-rb@2ba9010b91 (2026-08-23). Data as JSON: /api/errors/ca13a9217ee562ab. Report an issue: GitHub.

Appendix: source

Thrown at lib/redis.rb:302

    if @options.fetch(:protocol, 3).to_i == 3 && Client.resp3_unsupported?(error)
      @options = @options.merge(protocol: 2)
      @client.close
      @client = build_client
      # Warn only once the RESP2 client is actually in place — if the rebuild itself raises we
      # haven't really fallen back. Fires once per client: @options[:protocol] is now 2, so this
      # branch never re-enters. Passing `protocol: 2` explicitly skips it (and silences this).
      warn("Redis: the server does not support RESP3 (the HELLO 3 handshake failed); falling back " \
           "to RESP2. Pass `protocol: 2` to select RESP2 explicitly and silence this warning.")
      retry
    end

    raise
  end

  def _subscription(method, timeout, channels, block)
    if block
      if @subscription_client
        raise SubscriptionError, "This client is already subscribed"
      end

      begin
        # The pub/sub second socket is opened via @client.pubsub, which connects through
        # ensure_connected rather than a command path. Route it through #synchronize so the same
        # RESP3->RESP2 fallback applies when subscribe is the first operation against an old server.
        @subscription_client = SubscribedClient.new(synchronize(&:pubsub))
        if timeout > 0
          @subscription_client.send(method, timeout, *channels, &block)
        else
          @subscription_client.send(method, *channels, &block)
        end
      ensure
        @subscription_client&.close
        @subscription_client = nil
      end
    else
      unless @subscription_client

View on GitHub (pinned to 2ba9010b91)