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).

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)

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.