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
- Serialize reads: have a single consumer drain the stream, then distribute data to others.
- If you must tee, buffer the body once: `body = await request.read()` then reuse the bytes.
- Cancel competing read tasks before issuing a new read.
- 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
- Designate exactly one consumer per StreamReader.
- If you must fan-out, materialize body bytes once and share them.
- Use asyncio.Lock when consumers cannot be statically serialized.
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
- Concurrent call to receive() is not allowed
- Called while some coroutine is waiting for incoming data.
- Cannot write to closing transport
- Session and connector have to use same event loop
- Timeout context manager should be used inside a task
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/ecaf8b5b8bcaaaf9.json.
Report an issue: GitHub.