aio-libs/aiohttp · error · RuntimeError

() called while another coroutine is already waiting for…

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 an unresolved Future, meaning another coroutine is currently awaiting incoming data on the same reader. StreamReader links the protocol's feed_data to a single read coroutine via one waiter future; two concurrent readers would race unpredictably for each chunk, so aiohttp forbids it.

Solutions

  1. Serialize all reads of one stream through a single consumer task; do not share a StreamReader between concurrent coroutines.
  2. If you need fan-out, have one reader push chunks into an asyncio.Queue and have workers read from the queue.
  3. Read the whole body once (await response.read()) and then distribute the bytes.

Example fix

# before
async def worker(c):
    return await c.read(100)
await asyncio.gather(worker(content), worker(content))

# after
body = await response.read()  # single consumer
# split body between workers as needed
Defensive patterns

Strategy: validation

Validate before calling

if stream._waiter is not None and not stream._waiter.done():
    raise RuntimeError('another reader is active on this stream')
data = await stream.read(n)

Type guard

def stream_idle(stream) -> bool:
    w = getattr(stream, '_waiter', None)
    return w is None or w.done()

Try / catch

try:
    data = await stream.read(n)
except RuntimeError as e:
    if 'already waiting' in str(e):
        # serialize reads instead of concurrent
        raise
    raise

Prevention

When it happens

Trigger: Two tasks both calling await content.read(n) (or readline/readchunk) on the same response/request stream at the same time; a background task draining the body while the main task also reads; calling read while a previous read is still suspended.

Common situations: Spawning asyncio.gather over the same stream; a progress-monitoring task reading the body alongside the consumer; reusing a single response across multiple worker tasks.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/ecaf8b5b8bcaaaf9. Report an issue: GitHub.

Appendix: 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 d041d4d0fd)