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

Can't unsubscribe if not subscribed.

Error message

Can't unsubscribe if not subscribed.

What it means

Redis::Distributed tracks at most one active subscription via @subscribed_node, set by #subscribe and nil otherwise. #unsubscribe forwards to that node, so calling it when @subscribed_node is nil raises Redis::SubscriptionError ("Can't unsubscribe if not subscribed.") — the instance has no subscription to tear down. The same error class and message are used by the standalone client's subscription loop, so it predates Distributed. On Distributed it is purely a client-side state check: nothing is sent to any server.

Source

Thrown at lib/redis/distributed.rb:1281

      !!@subscribed_node
    end

    # Listen for messages published to the given channels.
    def subscribe(channel, *channels, &block)
      if channels.empty?
        @subscribed_node = node_for(channel)
        @subscribed_node.subscribe(channel, &block)
      else
        ensure_same_node(:subscribe, [channel] + channels) do |node|
          @subscribed_node = node
          node.subscribe(channel, *channels, &block)
        end
      end
    end

    # Stop listening for messages posted to the given channels.
    def unsubscribe(*channels)
      raise SubscriptionError, "Can't unsubscribe if not subscribed." unless subscribed?

      @subscribed_node.unsubscribe(*channels)
    end

    # Listen for messages published to channels matching the given patterns.
    # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/)
    # for further details
    def psubscribe(*channels, &block)
      raise NotImplementedError
    end

    # Stop listening for messages posted to channels matching the given
    # patterns.
    # See the [Redis Server PUNSUBSCRIBE documentation](https://redis.io/docs/latest/commands/punsubscribe/)
    # for further details
    def punsubscribe(*channels)
      raise NotImplementedError
    end

View on GitHub (pinned to 2ba9010b91)

Solutions

  1. Guard the call: dist.unsubscribe(*channels) if dist.subscribed?
  2. Make the unsubscribe path use the same Distributed instance that subscribed (pass the client through, or store it next to the subscription)
  3. For cleanup blocks, rescue Redis::SubscriptionError and treat it as already-clean

Example fix

# before
def shutdown(dist)
  dist.unsubscribe("news")
end
# => Redis::SubscriptionError: Can't unsubscribe if not subscribed.

# after
def shutdown(dist)
  dist.unsubscribe("news") if dist.subscribed?
end
Defensive patterns

Strategy: validation

Validate before calling

# Distributed exposes subscribed? exactly for this check
dist.unsubscribe(*channels) if dist.subscribed?

Type guard

def can_unsubscribe?(client)
  !client.respond_to?(:subscribed?) || client.subscribed?
end

Try / catch

begin
  dist.unsubscribe(*channels)
rescue Redis::SubscriptionError
  # already unsubscribed — nothing to do
end

Prevention

When it happens

Trigger: Calling dist.unsubscribe(*channels) before any dist.subscribe on that instance; after the subscribe block has already returned (subscription already closed); after an error interrupted the subscribe loop; in an ensure/cleanup block that runs whether or not a subscription exists; calling unsubscribe on a different Redis::Distributed instance than the one that subscribed.

Common situations: Worker shutdown hooks (SIGTERM handlers) that unconditionally unsubscribe; drain/cleanup code in ensure blocks; reconnect logic that tears down subscriptions it never established; multi-process forking where the child inherits the object but not the subscription state.

Related errors


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