python/cpython · error · RuntimeError
Empty waiter is already set
Error message
Empty waiter is already set
What it means
Raised by _SelectorSocketTransport._make_empty_waiter() as RuntimeError when an empty-waiter future already exists. The empty waiter is an internal future created at the start of a sendfile operation to wait for the write buffer to drain; a second one is only possible if sendfile is initiated while a previous sendfile is still pending, which the transport forbids.
Source
Thrown at Lib/asyncio/selector_events.py:1223
if self._buffer:
self._add_writer(self._sock_fd, self._write_ready)
self._maybe_pause_protocol()
def can_write_eof(self):
return True
def _call_connection_lost(self, exc):
try:
super()._call_connection_lost(exc)
finally:
self._write_ready = None
if self._empty_waiter is not None:
self._empty_waiter.set_exception(
ConnectionError("Connection is closed by peer"))
def _make_empty_waiter(self):
if self._empty_waiter is not None:
raise RuntimeError("Empty waiter is already set")
self._empty_waiter = self._loop.create_future()
if not self._buffer:
self._empty_waiter.set_result(None)
return self._empty_waiter
def _reset_empty_waiter(self):
self._empty_waiter = None
def close(self):
self._read_ready_cb = None
super().close()
class _SelectorDatagramTransport(_SelectorTransport, transports.DatagramTransport):
_header_size = 8
def __init__(self, loop, sock, protocol, address=None,View on GitHub (pinned to bc6749cc3b)
Solutions
- Serialize sendfile calls per transport: await each loop.sendfile() before starting the next.
- Protect the transport with an asyncio.Lock or a per-connection worker queue.
- Await/cancel outstanding sendfile tasks before starting a new one or closing the connection.
Example fix
// before
asyncio.create_task(loop.sendfile(transport, f1))
await loop.sendfile(transport, f2) # RuntimeError: Empty waiter is already set
// after
async with conn_lock:
await loop.sendfile(transport, f1)
await loop.sendfile(transport, f2) Defensive patterns
Strategy: validation
Validate before calling
file_lock = asyncio.Lock()
async def send_one_at_a_time(transport, paths):
for p in paths:
async with file_lock: # prevents overlapping sendfiles
with open(p, 'rb') as f:
await loop.sendfile(transport, f) Try / catch
try:
await loop.sendfile(transport, f)
except RuntimeError as e:
if 'Empty waiter' in str(e):
await prior_sendfile_task # drain previous sendfile, then retry
await loop.sendfile(transport, f)
else:
raise Prevention
- Await every sendfile call to completion before initiating another on the same transport.
- Pipelined file responses over one connection should go through a serialized sender task.
- Cancel in-flight sendfile tasks before closing or reusing a connection.
When it happens
Trigger: Two overlapping sendfile operations on the same transport: e.g. calling loop.sendfile(transport, f1) and, before it resolves, loop.sendfile(transport, f2); typically via concurrent tasks sharing one StreamWriter.
Common situations: A file-server handler processing pipelined requests concurrently over a single connection; retry logic that re-issues sendfile without awaiting the first; fire-and-forget task groups writing files to the same client connection.
Related errors
- unable to write; sendfile is in progress
- unable to writelines; sendfile is in progress
- d3-flame-graph library failed to load
- offset must be a non-negative integer (got {!r})
- Transport is closing
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/8740cb332bcf5ef8.
Report an issue: GitHub.