redis/redis-py · error · OSError

Buffer is closed.

Error message

Buffer is closed.

What it means

Raised by _AsyncRESPBase.can_read() (the async Python parser) when self._connected is False. can_read() is called by PubSub/health checks to detect pending data on a connection; calling it after on_disconnect() set _connected=False is undefined, so the parser raises OSError('Buffer is closed.') Note this is an OSError, not a ConnectionError - the connection object is in a terminal state and should not be reused.

Source

Thrown at redis/_parsers/base.py:549

        self._connected = True

    def on_disconnect(self):
        """Called when the stream disconnects"""
        self._connected = False

    @deprecated_function(
        version="8.0.0",
        reason="Use can_read() instead",
        name="can_read_destructive",
    )
    async def can_read_destructive(self) -> bool:
        return await self.can_read()

    async def can_read(self) -> bool:
        # TODO: Rename this API; it detects pending data or dirty/closed
        # connection state, not only whether application data can be read.
        if not self._connected:
            raise OSError("Buffer is closed.")
        if self._buffer:
            return True
        # asyncio.StreamReader has no public non-destructive API for checking
        # buffered bytes. Preserve dirty-connection detection for the Python
        # parser and fail loudly if the private buffer API changes.
        return bool(self._stream._buffer) or self._stream.at_eof()

    async def _read(self, length: int) -> bytes:
        """
        Read `length` bytes of data.  These are assumed to be followed
        by a '\r\n' terminator which is subsequently discarded.
        """
        want = length + 2
        end = self._pos + want
        if len(self._buffer) >= end:
            result = self._buffer[self._pos : end - 2]
        else:
            tail = self._buffer[self._pos :]

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Check pubsub.connection or the client's connection state before polling, or catch OSError and resubscribe on a fresh connection.
  2. Use the high-level pubsub context manager / reconnect helpers so a new connection is acquired automatically.
  3. Raise server tcp-keepalive/timeout values or send periodic PING commands to keep the connection alive.
  4. Do not reuse a PubSub or connection object after an explicit disconnect() or context-manager exit.

Example fix

// before
ps = r.pubsub()
await ps.subscribe("ch")
# ... connection drops, then:
msg = await ps.get_message()  # OSError: Buffer is closed.

// after
try:
    msg = await ps.get_message(timeout=1)
except OSError:
    await ps.close()
    ps = r.pubsub()
    await ps.subscribe("ch")
    msg = await ps.get_message(timeout=1)
Defensive patterns

Strategy: try-catch

Validate before calling

# Before polling pubsub, check connection liveness
async def pubsub_alive(ps):
    conn = getattr(ps, "connection", None)
    return conn is not None and getattr(conn, "_reader", None) is not None

Try / catch

try:
    msg = await ps.get_message(timeout=1)
except OSError as e:
    if "Buffer is closed" in str(e):
        await ps.close()
        ps = r.pubsub()
        await ps.subscribe(*channels)

Prevention

When it happens

Trigger: Calling pubsub.get_message(), connection.can_read(), or a pipeline health check on a connection that was already disconnected (server-initiated close, client.disconnect(), pool eviction, or 'with client:' context exit). The reader/writer were torn down and _connected flipped to False before can_read ran.

Common situations: Holding a PubSub object across a reconnect and calling get_message() before re-subscribing; using 'async with redis.Redis()' then awaiting a method after the block exited; server-side idle timeout (tcp-keepalive/tcp-timeout) dropping the connection between polls.

Related errors


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