redis/redis-py · error · ConnectionError

Connection closed by server.

Error message

Connection closed by server.

What it means

Raised by _HiredisParser.can_read() at the very first check: 'if not self._reader'. The reader is set in on_connect() and cleared in on_disconnect(); a falsy reader means the connection is not live. can_read() is called by PubSub and the connection-pool health check, so this surfaces when polling a disconnected connection. ConnectionError, error_type=NETWORK.

Source

Thrown at redis/_parsers/hiredis.py:163

        if connection.encoder.decode_responses:
            kwargs["encoding"] = connection.encoder.encoding
        self._reader = hiredis.Reader(**kwargs)

        try:
            self._hiredis_PushNotificationType = hiredis.PushNotification
        except AttributeError:
            # hiredis < 3.2
            self._hiredis_PushNotificationType = None

    def on_disconnect(self):
        self._sock = None
        self._reader = None

    def can_read(self, timeout: float = 0) -> 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._reader:
            raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)

        if self._reader.has_data():
            return True
        if not _socket_can_read(self._sock, timeout):
            return False
        # the socket reports readable but the reader has no buffered data. a
        # server-closed socket also reads as ready (it yields EOF), so tell the
        # two apart with a non-destructive poll: a peer-closed socket must not be
        # reused, while a readable-but-open socket may just hold a pending push.
        # this mirrors how the pure-Python parser (recv -> b"") and the async
        # parser (StreamReader.at_eof()) already signal a closed connection.
        if _socket_is_closed(self._sock):
            raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
        return True

    def read_from_socket(self, timeout=SENTINEL, raise_on_timeout=True):
        sock = self._sock
        reader = self._reader

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Do not reuse a PubSub/connection after disconnect; obtain a new one from the pool/client.
  2. Configure retry_on_error=[ConnectionError] with a backoff so transient drops reconnect transparently.
  3. Raise server tcp-keepalive/timeout or send periodic PINGs to keep the flow alive.
  4. Ensure forked children create their own Redis client.

Example fix

// before
ps = r.pubsub(ignore_subscribe_messages=True)
ps.subscribe("ch")
# ... server restarts ...
ps.get_message()  # ConnectionError: reader is None

// after
try:
    ps.get_message(timeout=1)
except redis.exceptions.ConnectionError:
    ps.close()
    ps = r.pubsub(ignore_subscribe_messages=True)
    ps.subscribe("ch")
Defensive patterns

Strategy: try-catch

Validate before calling

# Check the parser/reader is live before polling a sync pubsub
def pubsub_ready(ps):
    return ps.connection is not None and ps.connection._parser is not None

Try / catch

try:
    ps.get_message(timeout=1)
except redis.exceptions.ConnectionError as e:
    if "Connection closed by server" in str(e):
        ps.close()
        ps = r.pubsub(); ps.subscribe(*channels)

Prevention

When it happens

Trigger: Calling pubsub.get_message()/connection.can_read() on a hiredis-backed connection whose on_disconnect() already set self._reader=None - server closed, client.disconnect(), pool evicted it, or 'with client:' exited. Sibling of error 1 (which is the same check for the pure-Python async parser).

Common situations: Sync PubSub reused after a drop; pool health-check pinging a connection that another thread just disconnected; server idle-timeout killing the TCP flow; forking a process and reusing the parent's connection.

Related errors


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