{"record":{"id":"ca13a9217ee562ab","repo":"redis/redis-rb","slug":"this-client-is-already-subscribed","errorCode":null,"errorMessage":"This client is already subscribed","messagePattern":"This client is already subscribed","errorType":"exception","errorClass":"Redis::SubscriptionError","httpStatus":null,"severity":"error","filePath":"lib/redis.rb","lineNumber":302,"sourceCode":"    if @options.fetch(:protocol, 3).to_i == 3 && Client.resp3_unsupported?(error)\n      @options = @options.merge(protocol: 2)\n      @client.close\n      @client = build_client\n      # Warn only once the RESP2 client is actually in place — if the rebuild itself raises we\n      # haven't really fallen back. Fires once per client: @options[:protocol] is now 2, so this\n      # branch never re-enters. Passing `protocol: 2` explicitly skips it (and silences this).\n      warn(\"Redis: the server does not support RESP3 (the HELLO 3 handshake failed); falling back \" \\\n           \"to RESP2. Pass `protocol: 2` to select RESP2 explicitly and silence this warning.\")\n      retry\n    end\n\n    raise\n  end\n\n  def _subscription(method, timeout, channels, block)\n    if block\n      if @subscription_client\n        raise SubscriptionError, \"This client is already subscribed\"\n      end\n\n      begin\n        # The pub/sub second socket is opened via @client.pubsub, which connects through\n        # ensure_connected rather than a command path. Route it through #synchronize so the same\n        # RESP3->RESP2 fallback applies when subscribe is the first operation against an old server.\n        @subscription_client = SubscribedClient.new(synchronize(&:pubsub))\n        if timeout > 0\n          @subscription_client.send(method, timeout, *channels, &block)\n        else\n          @subscription_client.send(method, *channels, &block)\n        end\n      ensure\n        @subscription_client&.close\n        @subscription_client = nil\n      end\n    else\n      unless @subscription_client","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/redis/redis-rb/blob/2ba9010b91dab9e0fde1fbae3a9aae003f8bc307/lib/redis.rb#L284-L320","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Give each concurrent subscription its own Redis instance (Redis.new with the same URL)","Subscribe once to all channels in a single call — the block receives messages from every channel","Sequence subscriptions on one client: wait for the previous block to return (it exits after unsubscribe/timeout) before starting the next"],"exampleFix":"# before\nredis.subscribe('news') do |on|\n  on.message { |_, msg| redis.subscribe('alerts') { } }  # nested: raises already subscribed\nend\n\n# after\nredis.subscribe('news', 'alerts') do |on|\n  on.message { |channel, msg| handle(channel, msg) }\nend","handlingStrategy":"validation","validationCode":"SUB_LOCK = Mutex.new\n\ndef subscribe_exclusive(redis, *channels, &block)\n  raise Redis::SubscriptionError, 'another subscription is active' unless SUB_LOCK.try_lock\n  redis.subscribe(*channels, &block)\nensure\n  SUB_LOCK.unlock if SUB_LOCK.owned?\nend","typeGuard":null,"tryCatchPattern":"begin\n  redis.subscribe('ch') { |on| on.message { |_, m| handle(m) } }\nrescue Redis::SubscriptionError\n  subscriber = Redis.new(url: redis.connection[:host] ? config_url : config_url)\n  subscriber.subscribe('ch') { |on| on.message { |_, m| handle(m) } }\nend","preventionTips":["Use one dedicated Redis instance per concurrent subscription","Never call subscribe/psubscribe from inside a subscription block on the same client","Subscribe to all needed channels in one call — the block receives every message"],"tags":["pubsub","subscription","shared-client","concurrency"],"backgroundTag":"pubsub-already-subscribed","analyzedSha":"2ba9010b91dab9e0fde1fbae3a9aae003f8bc307","analyzedAt":"2026-08-23T03:54:57.017Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}