aio-libs/aiohttp · error · RuntimeError

%s() called while another coroutine is already waiting for i

Error message

%s() called while another coroutine is already waiting for incoming data

What it means

Raised by StreamReader._wait when self._waiter is already a pending future. aiohttp binds each chunk of incoming data to exactly one outstanding read coroutine; a second concurrent reader would make chunk delivery nondeterministic. Any of read/readany/readline/readchunk/readexactly calls that block at the same time therefore trip this RuntimeError. The message interpolates the offending method name via %s (func_name).

Source

Thrown at aiohttp/streams.py:358

        if len(self._http_chunk_splits) > self._high_water_chunks:
            self._protocol.pause_reading()

        # wake up readchunk when end of http chunk received
        waiter = self._waiter
        if waiter is not None:
            self._waiter = None
            set_result(waiter, None)

    async def _wait(self, func_name: str) -> None:
        if not self._protocol.connected:
            raise RuntimeError("Connection closed.")

        # StreamReader uses a future to link the protocol feed_data() method
        # to a read coroutine. Running two read coroutines at the same time
        # would have an unexpected behaviour. It would not possible to know
        # which coroutine would get the next data.
        if self._waiter is not None:
            raise RuntimeError(
                "%s() called while another coroutine is "
                "already waiting for incoming data" % func_name
            )

        waiter = self._waiter = self._loop.create_future()
        try:
            with self._timer:
                await waiter
        finally:
            self._waiter = None

    async def _fire_chunk_received(self, chunk: bytes) -> None:
        cb = self._on_chunk_received
        assert cb is not None
        # Run under the same per-stream timer that _wait() uses, so a hung
        # trace handler is bounded by sock_read just like a hung socket read would be.
        with self._timer:
            await cb(chunk)

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Serialize reads: have a single consumer drain the stream, then distribute data to others.
  2. If you must tee, buffer the body once: `body = await request.read()` then reuse the bytes.
  3. Cancel competing read tasks before issuing a new read.
  4. Use asyncio.Lock around stream access if you cannot guarantee single-consumer.

Example fix

// before
body = await asyncio.gather(
    request.content.read(),
    log_chunks(request.content),
)  # two concurrent readers
// after
body = await request.read()
await log_chunks_from(body)
Defensive patterns

Strategy: validation

Validate before calling

# guarantee single-consumer with a lock:
read_lock = asyncio.Lock()

async def safe_read(stream, n=-1):
    async with read_lock:
        return await stream.read(n)

Try / catch

try:
    return await stream.read(n)
except RuntimeError as e:
    if 'another coroutine is already waiting' in str(e):
        raise ConcurrentReadError('serialize stream reads') from e
    raise

Prevention

When it happens

Trigger: Two coroutines reading the same request.content simultaneously (e.g. one task reads the body, another logs chunks); awaiting request.read() while an asyncio Task also iterates request.content; mixing manual readchunk() with read() on the same stream.

Common situations: Background logging/metering task competing with the request handler for the body; firing multiple awaits in gather() against one stream; middleware that reads the body while the handler also does; accidental double-await of the same coroutine object.

Related errors


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