redis/redis-py · error · RuntimeError

pubsub connection not set: did you forget to call subscribe(

Error message

pubsub connection not set: did you forget to call subscribe() or psubscribe()?

What it means

Raised by PubSub.parse_response (redis/asyncio/client.py:1424) when self.connection is None. The pub/sub connection is only allocated by subscribe()/psubscribe()/unsubscribe(); calling parse_response() (or any path that drives reads) before subscribing has no socket to read from, so the library raises a RuntimeError with an explanatory hint.

Source

Thrown at redis/asyncio/client.py:1424

            block=False when a timeout is provided, and block=True when timeout=None.

        Example:
            # Block indefinitely (timeout is ignored)
            response = await pubsub.parse_response(block=True, timeout=0.1)

            # Non-blocking with 0.1 second timeout
            response = await pubsub.parse_response(block=False, timeout=0.1)

            # Non-blocking, return immediately
            response = await pubsub.parse_response(block=False, timeout=0)

            # Recommended: use get_message() instead
            msg = await pubsub.get_message(timeout=0.1)  # automatically sets block=False
            msg = await pubsub.get_message(timeout=None)  # automatically sets block=True
        """
        conn = self.connection
        if conn is None:
            raise RuntimeError(
                "pubsub connection not set: "
                "did you forget to call subscribe() or psubscribe()?"
            )

        await self.check_health()

        if not conn.is_connected:
            await conn.connect()

        # Block=True: signal "no timeout" to conn.read_response via
        # math.inf. The connection treats math.inf as the per-read
        # opt-in for blocking indefinitely without falling back to
        # self.socket_timeout. Reconnect/AUTH/HELLO/resubscribe
        # operations performed by the retry layer continue to honor
        # self.socket_timeout because they do not pass math.inf.
        #
        # TODO(next-major): when the async Connection.read_response
        # default for ``timeout`` is changed to SENTINEL, passing

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Call `await pubsub.subscribe(channel)` (or psubscribe) before invoking parse_response/get_message.
  2. Use get_message(timeout=...) which is the intended public API and is only entered after subscribing.
  3. Guard the read path: check `pubsub.connection is not None` or `pubsub.subscribed` before reading.

Example fix

// before
pubsub = r.pubsub()
resp = await pubsub.parse_response()
// after
pubsub = r.pubsub()
await pubsub.subscribe("ch")
resp = await pubsub.parse_response()
Defensive patterns

Strategy: validation

Validate before calling

pubsub = r.pubsub()
await pubsub.subscribe('ch')
assert pubsub.connection is not None
resp = await pubsub.parse_response()

Prevention

When it happens

Trigger: Calling `await pubsub.parse_response(...)` or `await pubsub.get_message(...)` on a PubSub object obtained from `client.pubsub()` before any subscribe()/psubscribe() call. The connection field stays None until a subscription allocates a dedicated connection from the pool.

Common situations: A consumer loop started before the subscription call completes; refactoring that reorders subscribe() after the read loop; tests that call parse_response directly to inspect raw frames.

Related errors


AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04). Data as JSON: /data/errors/e91ee1f85c5c7166.json. Report an issue: GitHub.