aio-libs/aiohttp · error · RuntimeError

Session is closed

Error message

Session is closed

What it means

Raised at the top of `ClientSession._request` (client.py:502-503) when `self.closed` is True. A session becomes closed after `.close()` completes or after exiting its `async with` block. Any subsequent call into `request/get/post/ws_connect/...` routes through `_request` and hits this guard before doing any work.

Source

Thrown at aiohttp/client.py:503

        proxy: StrOrURL | None = None,
        timeout: ClientTimeout | _SENTINEL | None = sentinel,
        ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel,
        server_hostname: str | None = None,
        proxy_headers: LooseHeaders | None = None,
        trace_request_ctx: object = None,
        read_bufsize: int | None = None,
        auto_decompress: bool | None = None,
        max_line_size: int | None = None,
        max_field_size: int | None = None,
        max_headers: int | None = None,
        middlewares: Sequence[ClientMiddlewareType] | None = None,
    ) -> ClientResponse:
        # NOTE: timeout clamps existing connect and read timeouts.  We cannot
        # set the default to None because we need to detect if the user wants
        # to use the existing timeouts by setting timeout to None.

        if self.closed:
            raise RuntimeError("Session is closed")

        method = method.upper()

        if ssl is sentinel:
            ssl = self._default_ssl
        if not isinstance(ssl, SSL_ALLOWED_TYPES):
            raise TypeError(
                "ssl should be SSLContext, Fingerprint, or bool, "
                f"got {ssl!r} instead."
            )

        if data is not None and json is not None:
            raise ValueError(
                "data and json parameters can not be used at the same time"
            )
        elif json is not None:
            if self._json_serialize_bytes is not None:
                data = payload.JsonBytesPayload(json, dumps=self._json_serialize_bytes)

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Ensure all requests finish before the session's `async with` block exits or before `await session.close()`.
  2. Restructure so the session owns the lifetime of the response (read/consume the body inside the context).
  3. Guard call sites: `if not session.closed: await session.get(...)`.

Example fix

// before
async with aiohttp.ClientSession() as s:
    resp = await s.get(url)
return await resp.read()  # block already exited -> Session is closed on next use
// after
async with aiohttp.ClientSession() as s:
    resp = await s.get(url)
    return await resp.read()  # consume inside the context
Defensive patterns

Strategy: try-catch

Validate before calling

def ensure_open(session):
    if session.closed:
        raise RuntimeError('ClientSession is closed; create a new one')
    return session

Type guard

def is_session_usable(session) -> bool:
    return not session.closed

Try / catch

try:
    resp = await session.get(url)
except RuntimeError as e:
    if str(e) == 'Session is closed':
        session = await new_session()  # rebuild
        resp = await session.get(url)
    else:
        raise

Prevention

When it happens

Trigger: Using the session after `await session.close()`, after the `async with aiohttp.ClientSession() as s:` block exits, or after the connector was closed separately. Also from a `__del__`-triggered path or a fire-and-forget task that outlives the session scope.

Common situations: Returning a ClientResponse from an `async with` block and awaiting it later; background tasks holding a session reference after the app shutdown closed it; test fixtures closing the session in teardown while a coroutine is still pending.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/37192122b7632643.json. Report an issue: GitHub.